Unit Testing con Mockito

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 metodi when();
  • 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.

Per mockare il Dao ed eseguire comunque i metodi del service senza interagire con il db il codice necessario è il seguente:

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:

Questa volta l’ultima riga stamperà la stringa Result: true.

Nella definizione del comportamento riconosciamo due elementi fondamentali:

  1. La regola di matching utilizzata per identificare l’input del metodo (anyString() nel nostro esempio).
  2. 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.

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:

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:

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:

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à:

  1. Invocando il metodo statico MockitoAnnotations.initMocks(this) in un metodo annotato con @Before.
  2. Utilizzando una proprietà di tipo MockitoRule annotata con @Rule nella classe di test.
  3. 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:

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.

© 2019 Java Boss - Theme by HappyThemes