Introduzione
Lo scopo dei test unitari è quello di verificare il funzionamento di determinate porzioni di codice. Lo standard di fatto per l’esecuzione dei test sul codice java è il framework JUnit, che consente di implementare test che possono essere eseguiti in modo automatico e ripetuti nel tempo.
Caratteristica fondamentale dei test unitari è la possibilità di poter testare il codice in modo isolato e limitando il più possibile situazioni di side effect. Questo al fine di evitare che l’esecuzione del test porti a stati di inconsistenza che ne impediscano la ripetitività nel tempo.
Caso tipico è quello in cui l’esecuzione del test comporta l’aggiornamento di informazioni su una base dati; operazione che se in generale non è desiderabile, in quanto risultato di un test, dall’altro potrebbe modificare le condizioni di test rendendone impossibile l’esecuzione ripetuta.
Mock Object
Nella programmazione orientata agli oggetti l’isolamento dei test unitari può essere ottenuto utilizzando oggetti che simulano il comportamento degli oggetti reali, denominati mock object (oggetti mock). Il concetto di mock object è, in realtà, utilizzato anche nei casi in cui si intenda differire l’implementazione di determinate classi, consentendo allo sviluppatore di focalizzarsi su determinati aspetti di un programma.
Per i nostri scopi, invece, utilizzeremo i mock object nei test unitari al fine di simulare il comportamento di oggetti complessi e non utilizzabili. Ovviamente l’oggetto mock deve esporre la stessa interfaccia dell’oggetto che simula, consentendo al client di ignorare se sta interagendo con l’oggetto reale o con quello simulato.
L’implementazione di un oggetto mock può in generale essere un’operazione complessa e comunque produrrebbe un codice finalizzato esclusivamente ai test e che quindi non verrebbe mai portato in produzione. Per tale motivo sono nati framework che consentono di creare oggetti mock in modo rapido.
Mockito
Mockito è uno dei framework più popolari per la realizzazione di oggetti mock. Consente di generare un mock a partire sia da una interfaccia che da un classe semplicemente dichiarandone il comportamento, ed inoltre permette di eseguire diverse tipologie di test. Prima di procedere con gli esempi vediamo rapidamente quali sono i costrutti fondamentali del framework:
mock()
o@Mock
: consentono la definizione di un oggetto mock al quale è poi possibile associare un comportamento ad esempio utilizzando i metodiwhen()
;spy()
o@Spy
: consentono di realizzare mock parziale dell’oggetto permettendo quindi di invocare metodi reali;verify()
: consente di verificare (testare) la corretta invocazione dei metodi.
L’esempio
Nell’esempio che presentiamo sono definite due classi: una classe DaoClass
, che implementa la logica di accesso al database, ed una classe ServiceClass
che implementa i servizi che intendiamo testare. La classe Dao sarà quella oggetto del mock.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class DaoClass { public boolean query(String query) { return isSelect(query) ? select(query) : update(query); } private boolean isSelect(String query) { return query.startsWith("select"); } public boolean select(String select) { //... // return true; } public boolean update(String update) { //... // return true; } } |
1 2 3 4 5 6 7 8 9 10 11 |
public class ServiceClass { private DaoClass dao; public ServiceClass(DaoClass dao) { this.dao = dao; } public boolean query(String query) { return dao.query(query); } } |
Per mockare il Dao ed eseguire comunque i metodi del service senza interagire con il db il codice necessario è il seguente:
1 2 3 4 |
DaoClass dao = Mockito.mock( DaoClass.class ); ServiceClass service = new ServiceClass(dao); boolean result = service.query( "select * from t" ); System.out.println( "Result: " + result ); |
La prima riga genera l’oggetto mock del dao, di fatto creando un proxy dinamico della classe DaoClass
, il quale è poi utilizzato nel costruttore del service nella riga successiva. La terza riga esegue il metodo query()
della classe ServiceClass
il quale a sua volta invoca l’omonimo metodo della classe DaoClass
. Essendo però il dao mockato i metodi implementati nella classe DaoClass
non sono realmente invocati ma il framework, in assenza della definizione del comportamento atteso dal mock, restituisce un risultato di default che in generale è:
null
per gli oggetti;0
per i numeri;false
per i booleani;- collezioni vuote per le collection;
- etc.
Conseguentemente l’ultima riga stamperà semplicemente la stringa Result: false
sebbene l’implementazione della classe DaoClass
preveda sempre come risultato true
.
Modifichiamo il comportamento del mock inserendo prima dell’invocazione service.query()
la seguente riga di codice, che istruisce il framework indicando che, indipendentemente dalla query di input, l’output del metodo query()
della classe DaoClass
è sempre true
:
1 |
Mockito.when( dao.query( Mockito.anyString() ) ).thenReturn( true ); |
Result: true
.
Nella definizione del comportamento riconosciamo due elementi fondamentali:
- La regola di matching utilizzata per identificare l’input del metodo (
anyString()
nel nostro esempio). - Il valore restituito dal metodo per l’input specificato (
thenReturn(true)
nel nostro caso).
Ovviamente il framework offre diverse possibilità che non riportiamo nell’articolo per brevità, ma si rimanda al link https://dzone.com/refcardz/mockito dove sono presenti diverse tabelle riepilogative di grande utilità.
Mock Parziale
Nell’esempio che abbiamo presentato la classe DaoClass
è completamente mockata, ovvero nessuno dei suoi metodi è mai invocato realmente e conseguentemente il comportamento di ciascuno di essi deve essere eventualmente dichiarato in modo esplicito al framework.
Questo può essere evitato eseguendo il mock parziale della classe attraverso il costrutto spy()
, il quale permette di invocare i metodi reali dell’oggetto continuando comunque a tracciarne le invocazioni. Il comportamento di tali metodo può ancora essere ridefinito utilizzando la stessa logica definita per mock()
.
Riscriviamo quindi l’esempio precedente utilizzando questa volta l’istruzione Mockito.spy( DaoClass.class )
, come mostrato di seguito.
1 2 3 4 |
DaoClass dao = Mockito.spy( DaoClass.class ); ServiceClass service = new ServiceClass(dao); boolean result = service.query( "select * from t" ); System.out.println( "Result: " + result ); |
Questa volta il metodo query()
della classe DaoClass
è invocato realmente e conseguentemente l’ultima riga stamperà la stringa Result: true
. Possiamo comunque modificarne il comportamento ad esempio con l’istruzione:
1 |
Mockito.when( dao.select( Mockito.anyString() ) ).thenReturn( false ); |
Verifica
Un’altra importante caratteristica del framework Mokito p la sua capacità di tenere traccia di tutte le chiamate, e dei relativi parametri di invocazione, eseguite sull’oggetto mockato. Questo consente di eseguire quelli che vengono generalmente indicati come behavior testing o test di comportamento. Tali test non hanno come scopo quello di controllare il risultato di una chiamata di metodo, ma di verificare che un metodo sia chiamato con i parametri corretti. A tale scopo il framework dispone del costrutto verify()
utilizzabile sull’oggetto mock e che permette di verificare che determinate condizioni specificate siano soddisfatte.
Utilizzando le nostre classi di test un possibile utilizzo del costrutto è il seguente:
1 |
Mockito.verify( dao ).select(Mockito.anyString()); |
Nel caso in cui il metodo sull’oggetto mock è effettivamente invocato l’istruzione non provoca nessun effetto visibile. Diversamente se non vi è traccia dell’esecuzione del metodo, una eccezione sarà sollevata e in consolle troveremo un messaggio del tipo:
1 2 3 |
Exception in thread "main" Wanted but not invoked: daoClass.update(<any>); -> at it.javaboss.mock.MockExample.main(MockExample.java:14) |
Integrazione con JUnit
Vediamo infine come sia possibile integrare il framework Mokito con JUnit, attraverso l’uso delle annotazioni @Mock
e @Spy
, utilizzate similmente ai metodi mock()
e spy()
per dichiarare gli oggetti da mockare. L’attivazione di tali annotazioni può avvenire un tre distinte modalità:
- Invocando il metodo statico
MockitoAnnotations.initMocks(this)
in un metodo annotato con@Before
. - Utilizzando una proprietà di tipo
MockitoRule
annotata con@Rule
nella classe di test. - Annotando la classe di test con
@RunWith(MockitoJUnitRunner.class)
.
Riportando quindi il codice descritto nei paragrafi precedenti all’interno di un test unitario di JUnit si ottiene quindi:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class MockitoTest1 { @Mock DaoClass dao; @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); private ServiceClass service; @Before public void init() { service = new ServiceClass(dao); when(dao.query(anyString())).thenReturn(true); } @Test public void testQuery() { boolean check = service.query("select * from t"); assertTrue(check); verify(dao).query("select * from t"); } } |
Nel codice allegato all’articolo è presente anche un esempio con l’annotazione @Spy
.
Codice Sorgente
Il codice sorgente completo degli esempi descritti nell’articolo può essere scaricato qui mockito.