Completiamo la nostra discussione su Dagger, affrontando l’organizzazione delle classi e la testabilità del codice. Dagger ha alcune funzionalità avanzate che consentono di svolgere questi compiti in modo ottimale, e che vedremo in questo articolo.
Introduzione
Nel precedente articolo su Dagger abbiamo visto un esempio composto da alcune classi Java che permettono di scaricare una lista di repository dai servizi messi a disposizione da GitHub e di stamparli su console. Abbiamo messo particolare attenzione a scrivere codice facilmente testabile ma sono rimasti aperti un paio di punti. In particolare l’organizzazione delle classi può essere migliorata ancora per permettere di testare le funzionalità in isolamento e per verificare se i risultati ottenuti sono corretti.
In questo articolo vedremo come sfruttare alcune funzionalità avanzate di Dagger per raggiungere questi obiettivi. Il codice sorgente dell’esempio che usiamo in questi due articoli è scaricabile da GitHub [1] o dal menu “Allegati” a destra.
Moduli di test
Fino ad adesso, nei test abbiamo costruito gli oggetti manualmente senza crearli con Dagger. Non era un grosso problema visto che gli oggetti considerati sono molto semplici e non avevamo quindi la necessità di scomodare Dagger. In progetti veri, però, può convenire sfruttare Dagger anche in questi casi per evitare di scrivere molto codice per creare l’albero delle dipendenze manualmente.
La feature di Dagger che ci può essere di aiuto è costituita dai moduli di test. Definendo un modulo di test è possibile sovrascrivere alcuni oggetti in un object graph, per esempio per sostituire alcuni oggetti con degli stub. Ovviamente tutti gli altri oggetti non hanno bisogno di modifiche in quanto la creazione e il collegamento fra i vari oggetti sono gestiti da Dagger.
In pratica un modulo di test è molto simile a un normale modulo, una classe annotata con @Module, ma contiene solitamente alcuni attributi aggiuntivi. Nel nostro caso abbiamo il seguente modulo di test che permette di sostituire l’istanza di RepositoryService con una istanza di RepositoryServiceStub:
@Module(injects = RepositoryListUiBeanTest.class, overrides = true, includes = CnjModule.class) public class TestModule { @Singleton @Provides public RepositoryService provideRepositoryServiceStub() { return new RepositoryServiceStub(); } }
Gli attributi
Vediamo nel dettaglio gli attributi che abbiamo utilizzato per configurare questo modulo:
- injects: l’avevamo già incontrato ed è uno degli attributi più usati in un modulo. Permette di definire gli oggetti del grafo che saranno accedibili dall’esterno. In pratica quelli su cui sarà possibile accedere attraverso il metodo get di un ObjectGraph. In questo caso abbiamo specificato la classe di test, fra poco vedremo il motivo.
- overrides: solitamente Dagger controlla durante la fase di compilazione che non siano definiti due volte oggetti dello stesso tipo. Nel caso di un modulo di test, usiamo questo attributo per poter sostituire un oggetto con il nostro stub.
- addsTo: Dagger esegue una validazione del grafo degli oggetti definiti nel modulo per controllare che non ci siano dipendenze non definite o oggetti inutilizzati. In questo caso, se consideriamo solo gli oggetti di questo modulo, il grafo sarebbe incompleto: per questo stiamo usando l’attributo addsTo per specificare l’altro modulo da usare per creare un grafo completo.
- Per “pilotare” la validazione del grafo di oggetti possiamo utilizzare anche altri due attributi: library e complete. Con il primo stiamo dicendo che il modulo definisce oggetti di una libreria e quindi non deve essere dato un errore nel caso di oggetti non usati. Con complete stiamo specificando di non dare errore se ci sono dipendenze non definite; ovviamente a runtime le dipendenze dovranno essere definite in un altro modulo.
Il test JUnit
A questo punto possiamo scrivere il test JUnit che sfrutta il modulo di test appena visto:
public class RepositoryListUiBeanTest { @Inject RepositoryListUiBean repositoryListUiBean; @Before public void setUp() { ObjectGraph.create(new TestModule()).inject(this); } @Test public void testPrintRepositories() throws Exception { repositoryListUiBean.printRepositories(); } }
Il metodo setUp merita un’attenzione particolare; infatti possiamo notare la chiamata al metodo inject che ancora non avevamo incontrato. In pratica inject esegue il popolamento di tutti i field annotati in base agli oggetti definiti nell’object graph. Il comportamento è molto simile al metodo get che abbiamo già visto ma, al contrario di questo, il metodo inject non si occupa anche di creare l’oggetto ma solo di popolare le dipendenze. Il metodo inject è fondamentale quando vogliamo usare Dagger in contesti in cui non abbiamo il controllo della creazione degli oggetti. Si pensi per esempio ai backing bean JSF, alle Activity e ai Fragment di Android o, come in questo caso, ai test di JUnit.
Una volta caricate tutte le dipendenze, nel nostro caso un oggetto repositoryListUiBean, ci resta solo di invocare il metodo per stampare la lista dei repository. Avendo definito nel modulo di test lo stub, eseguendo questo test non viene eseguita nessuna chiamata al server.
Stub per eseguire test in isolamento
Nel precedente paragrafo siamo risusciti a scrivere un test della fase di stampa dei repository isolando il metodo da testare grazie all’uso di uno stub. Ancora però non siamo in grado di testare la fase di parsing dei dati senza eseguire la chiamata al server. Per poter scrivere questo ulteriore test introduciamo una nuova classe che si occupa esclusivamente di effettuare la chiamata al server:
public class HttpDownloader { @Inject public HttpDownloader() { } public String download(String url) throws IOException { StringBuilder response = new StringBuilder(); try (BufferedReader in = new BufferedReader(new InputStreamReader(getInputStream(url)))) { String inputLine; while ((inputLine = in.readLine()) != null) { response.append(inputLine); } } return response.toString(); } protected InputStream getInputStream(String url) throws IOException { URLConnection connection = new URL(url).openConnection(); return connection.getInputStream(); } }
In pratica, abbiamo spostato i due metodi per connettersi al server e leggere lo stream dei dati dalla classe RepositoryService a questa nuova classe. Di conseguenza la classe RepositoryService avrà un campo con l’injection di questo nuovo oggetto e lo richiamerà all’interno del metodo retrieveRepositories:
@Singleton public class RepositoryService { @Inject HttpDownloader httpDownloader; public Repository[] retrieveRepositories() throws IOException { Gson gson = new Gson(); return gson.fromJson(httpDownloader.download( "https://api.github.com/repositories"), Repository[].class); } }
Abbiamo già parlato di stub: l’introduzione di questa nuova classe è stata necessaria proprio per poter creare uno stub per sostituire esclusivamente la chiamata al server:
public class HttpDownloaderStub extends HttpDownloader { @Override protected InputStream getInputStream(String url) throws IOException { return getClass().getResourceAsStream("/github.json"); } }
Aspetti importanti
Ci sono alcuni dettagli importanti da notare:
- non abbiamo creato una interfaccia, ma abbiamo esteso direttamente l’oggetto reale;
- abbiamo sovrascritto solo uno dei due metodi della classe base; infatti vogliamo che la chiamata al server sia sostituita con la lettura di un file, ma vogliamo comunque mantenere la trasformazione dello stream in una stringa;
- il metodo getInputStream dovrebbe essere private, ma lo abbiamo definito protected proprio per poterlo sovrascrivere nel test.
Una soluzione alternativa poteva essere quella di creare due oggetti separati (un downloader e uno streamParser) e di creare lo stub solo di uno dei due oggetti. Le due soluzioni sono equivalenti in molti aspetti; probabilmente la soluzione più pulita sarebbe stata quella di creare due classi. In questo caso abbiamo optato per una classe unica: i due metodi sono comunque brevi e la complessità è bassa anche con una unica classe.
A questo punto nel modulo di test possiamo definire esclusivamente l’oggetto HttpDownloader (non è più necessario creare lo stub del service):
@Module(injects = {RepositoryListUiBeanTest.class, RepositoryServiceTest.class}, overrides = true, includes = CnjModule.class) public class TestModule { @Singleton @Provides public HttpDownloader provideHttpDownloaderStub() { return new HttpDownloaderStub(); } }
La definizione dei test è la stessa vista in precedenza; stavolta possiamo testare in isolamento anche il service e controllare il risultato (in questo esempio il controllo è banale ma avremmo potuto scrivere assert più complessi):
@Test public void testRetrieveRepositories() throws Exception { Repository[] repositories = repositoryService.retrieveRepositories(); Assert.assertNotNull(repositories); }
Uso delle interfacce
Lavorare con le interfacce è una delle best practices più utilizzata in Java per avere un maggiore disaccoppiamento delle classi. Probabilmente, nel corso degli anni, questa pratica ha perso un po’ di importanza nei progetti in cui abbiamo il controllo di tutto il codice: con un buon IDE, usando un refactoring da pochi click, si riesce a introdurre una interfaccia anche in un secondo momento senza troppi problemi. Questa cosa non è vera se stiamo scrivendo una libreria: in quel caso un refactoring su tutto il codice che usa le classi della libreria è impossibile e quindi dobbiamo porre un’attenzione particolare.
Nell’esempio visto in questo articolo non abbiamo ancora usato interfacce: in Dagger si può lavorare sia con interfacce e implementazione, che direttamente con le classi. Per disaccoppiare l’oggetto RepositoryListUiBean dalla scrittura sullo standard output, introduciamo una interfaccia:
public interface StringWriter { void println(String s); }
L’implementazione che scrive sullo standard output è banale:
public class SystemOutStringWriter implements StringWriter { @Override public void println(String s) { System.out.println(s); } }
Avendo definito una interfaccia, dobbiamo definire l’implementazione che vogliamo usare in un modulo (non possiamo usare l’annotation inject come negli altri casi):
@Singleton @Provides public StringWriter provideStringWriter() { return new SystemOutStringWriter(); }
In questo modo si perde un po’ di semplicità: infatti, non potendo usare l’annotation inject, se abbiamo bisogno di dipendenze, dobbiamo passarle, di solito attraverso un costruttore custom, all’interno del metodo annotato con Provides.
Avendo introdotto nuove classi in questo articolo, il grafo degli oggetti è leggermente più complicato ma adesso coesione e disaccoppiamento delle classi sono migliorati, come si vede in figura 1.
Figura 1 – Il nuovo grafo degli oggetti, leggermente più articolato del precedente.
Uso dei mock in un progetto Dagger
Fino ad adesso non abbiamo verificato il testo scritto sullo standard output: il primo passo per aggiungere questo controllo è stato l’introduzione dell’interfaccia StringWriter. Il secondo passo sarà quello di introdurre l’utilizzo di un mock. La differenza fra stub e mock è abbastanza sottile ed è facile fare confusione fra i due tipi di oggetti. Tutti e due servono per testare in isolamento il comportamento di una classe sostituendo gli oggetti con cui collabora.
Come abbiamo visto più volte nel corso di questo articolo uno stub viene comunemente usato per fornire dei dati fittizi a una classe di test. Usando un mock, invece, si verifica che i dati che l’oggetto testato passa ad altri oggetti siano corretti.
I mock possono essere scritti a mano (per esempio salvando in un campo i parametri passati a un metodo) o utilizzando una libreria. In questo esempio utilizziamo Mockito [2], una libreria open source molto utilizzata. Aggiungiamo al test module la definizione dell’oggetto mock creato usando il metodo statico Mockito.mock a cui passiamo l’interfaccia da implementare:
@Singleton @Provides public StringWriter provideStringWriter() { return Mockito.mock(StringWriter.class); }
L’oggetto restituito è una implementazione fittizia della nostra interfaccia: in pratica è come se contenesse tutti i metodi vuoti. Questo oggetto però può essere utilizzato principalmente per due ragioni:
- Tramite alcuni metodi di utilità di Mockito, possiamo definire i valori di ritorno di un metodo; per esempio, con il codice when(mockedList.get(0)).thenReturn(“first”), definiamo il valore di ritorno del metodo get invocato passando il parametro 0. In pratica, in questo modo possiamo usare Mockito per creare al volo degli stub. In rete ci sono pareri discordanti su questo utilizzo: gli stub solitamente sono creati in classi esterne al test per facilitarne il riutilizzo su più test; definendo lo stub dentro il test il riuso del codice è più complicato.
- Usando il metodo verify della classe Mockito è possibile specificare e controllare le interazioni che ci aspettiamo con il mock (quali metodi saranno richiamati e con quali parametri).
Il test
Nel nostro caso creiamo un test in cui abbiamo due field, popolati con Dagger, corrispondenti all’oggetto da testare e al mock:
public class RepositoryListUiBeanTest { @Inject RepositoryListUiBean repositoryListUiBean; @Inject StringWriter stringWriterMock; @Before public void setUp() { ObjectGraph.create(new TestModule()).inject(this); } @Test public void testPrintRepositories() throws Exception { repositoryListUiBean.printRepositories(); verify(stringWriterMock, times(3)).println(anyString()); } }
Nel metodo di test abbiamo specificato che il metodo println della nostra interfaccia StringWriter deve essere invocato tre volte con un qualunque parametro di tipo stringa. Da notare che abbiamo potuto cablare nel codice il valore 3 in quanto, utilizzando lo stub, siamo sicuri dei dati su cui stiamo eseguendo il test. Usando un mock stiamo testando i risultati di un metodo che ha come input i valori forniti da uno stub.
Altre funzionalità di Dagger
Fino ad adesso abbiamo visto le principali feature che Dagger mette a disposizione, ma ovviamente ci sono altri aspetti del framework che possiamo sfruttare.
Lazy
Nel caso in cui un oggetto sia usato solo in alcuni casi, possiamo sfruttare la classe Lazy per evitare l’injection dell’oggetto collegato (e di conseguenza anche la creazione dell’oggetto e di tutte le sue dipendenze). In pratica possiamo definire un field di tipo Lazy: l’oggetto vero e proprio sarà istanziato e restituito richiamando il metodo get.
Provider
Nel caso in cui vogliamo creare più istanze di un oggetto sfruttando Dagger, possiamo richiedere l’injection di un oggetto Provider. Anche in questo caso abbiamo a disposizione il metodo get, ma, a differenza di Lazy, usando un oggetto Provider sarà restituito un nuovo oggetto a ogni invocazione del metodo get.
enum
È presente una enum impostabile sull’annotation Provides per definire un insieme di oggetti e non un singolo oggetto. Quando sarà richiesta l’injection, avremo quindi a disposizione l’insieme di oggetti (tutti dello stesso tipo) definiti in uno o più moduli.
Campi statici
Anche se ne è sconsigliato l’utilizzo, è comunque possibile eseguire l’injection anche su campi statici.
plus
Abbiamo visto che un oggetto può essere definito come singleton; in Dagger non sono presenti altri scope ma è possibile crearli al volo. Infatti usando il metodo plus di un ObjectGraph possiamo estendere un grafo aggiungendo gli oggetti definiti in altri moduli. L’utilizzo non è immediato da capire, ma questo metodo è molto potente e ci permette di creare object graph annidati (si pensi agli scope Application/Session/Request nel mondo web o Application/Activity/Fragment in Android) definendo oggetti singleton in un determinato scope.
La semplicità come scelta progettuale
Come detto all’inizio del primo articolo, Dagger è volutamente semplice. Il fatto che non ci siamo molte feature tipiche di altri framework di dependency injection è una scelta progettuale. Alcune cose non sono definite dal framework, ma possono essere simulate facilmente. Per esempio non possiamo definire un metodo da invocare dopo la injection (che invece è definibile con un postConstruct in altri framework). Nel caso in cui sia necessario eseguire del codice dopo l’injection possiamo comunque seguire due strade:
- usare una injection sul costruttore invece che sui field: in questo modo gli oggetti sono passati da Dagger al costruttore e quindi possiamo aggiungere della logica custom da eseguire sfruttando tali oggetti;
- usare i moduli, poichè i metodi annotati con Provides all’interno di un modulo possono contenere anche una logica di creazione custom; di solito contengono semplicemente la creazione di un oggetto ma, in caso di necessità, possiamo aggiungere in questi metodi anche della logica da eseguire (e, volendo, essa può essere dipendente da altri oggetti definiti con Dagger).
Conclusioni
In questi due articoli abbiamo visto nel dettaglio come utilizzare Dagger per gestire più classi all’interno di un progetto. L’esempio analizzato era molto semplice ma i concetti visti si possono adattare facilmente anche a contesti reali.
Le caratteristiche di Dagger lo rendono adatto a qualunque tipo di utilizzo; infatti, basandosi sulla generazione di codice, non abbiamo overhead in termini di performance. Rispetto a framework più complessi (per esempio Spring) ha ovviamente qualcosa in meno; per esempio, non essendo basato su proxy delle classi, tutta la parte di aspect oriented non è presente.
Ma quello che si perde per questi limiti, dovuti a una precisa scelta progettuale, si acquista però sicuramente in semplicità, sia del codice da scrivere che, soprattutto, della configurazione del tutto.
Riferimenti
[1] Il codice dell’esempio presentato nei due articoli, su GitHub
https://github.com/fabioCollini/CoseNonJavisteDagger
[2] Mockito, un framework open source per la realizzazione di mock
https://code.google.com/p/mockito/