Una guida pratica per i test di unità al Mockito quotidiano
Pubblicato: 2022-03-11Il test unitario è diventato obbligatorio nell'era dell'Agile e ci sono molti strumenti disponibili per aiutare con i test automatizzati. Uno di questi strumenti è Mockito, un framework open source che consente di creare e configurare oggetti simulati per i test.
In questo articolo, tratteremo la creazione e la configurazione di mock e il loro utilizzo per verificare il comportamento previsto del sistema in fase di test. Ci immergeremo anche un po' negli interni di Mockito per comprenderne meglio il design e le avvertenze. Useremo JUnit come framework di unit test, ma poiché Mockito non è legato a JUnit, puoi seguire anche se stai usando un framework diverso.
Ottenere Mockito
Ottenere Mockito è facile in questi giorni. Se stai usando Gradle, si tratta di aggiungere questa singola riga al tuo script di build:
testCompile "org.mockito:mockito−core:2.7.7"
Per quanto riguarda quelli come me che preferiscono ancora Maven, aggiungi semplicemente Mockito alle tue dipendenze in questo modo:
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>2.7.7</version> <scope>test</scope> </dependency>
Naturalmente, il mondo è molto più ampio di Maven e Gradle. Sei libero di utilizzare qualsiasi strumento di gestione dei progetti per recuperare l'artefatto jar Mockito dal repository centrale di Maven.
Si avvicina a Mockito
Gli unit test sono progettati per testare il comportamento di classi o metodi specifici senza fare affidamento sul comportamento delle loro dipendenze. Poiché stiamo testando la più piccola "unità" di codice, non è necessario utilizzare implementazioni effettive di queste dipendenze. Inoltre, utilizzeremo implementazioni leggermente diverse di queste dipendenze durante il test di comportamenti diversi. Un approccio tradizionale e ben noto a questo è quello di creare "stub", implementazioni specifiche di un'interfaccia adatta a un determinato scenario. Tali implementazioni di solito hanno una logica codificata. Uno stub è una specie di doppio test. Altri tipi includono falsi, derisioni, spie, manichini, ecc.
Ci concentreremo solo su due tipi di doppi di prova, "beffe" e "spie", poiché questi sono ampiamente utilizzati da Mockito.
Scherzi
Cos'è deridere? Ovviamente, non è qui che prendi in giro i tuoi colleghi sviluppatori. La presa in giro per lo unit test è quando si crea un oggetto che implementa il comportamento di un sottosistema reale in modi controllati. In breve, i mock sono usati come sostituti di una dipendenza.
Con Mockito, crei un mock, dici a Mockito cosa fare quando vengono chiamati metodi specifici su di esso, quindi usi l'istanza mock nel tuo test invece della cosa reale. Dopo il test, puoi interrogare il mock per vedere quali metodi specifici sono stati chiamati o controllare gli effetti collaterali sotto forma di stato modificato.
Per impostazione predefinita, Mockito fornisce un'implementazione per ogni metodo del mock.
Spie
Una spia è l'altro tipo di doppio test creato da Mockito. Contrariamente alle derisioni, la creazione di una spia richiede un'istanza da spiare. Per impostazione predefinita, una spia delega tutte le chiamate di metodo all'oggetto reale e registra quale metodo è stato chiamato e con quali parametri. Questo è ciò che lo rende una spia: spiare un oggetto reale.
Prendi in considerazione l'uso di derisioni invece di spie quando possibile. Le spie potrebbero essere utili per testare codice legacy che non può essere riprogettato per essere facilmente testabile, ma la necessità di utilizzare una spia per deridere parzialmente una classe è un indicatore del fatto che una classe sta facendo troppo, violando così il principio di responsabilità unica.
Costruire un semplice esempio
Diamo un'occhiata a una semplice demo per la quale possiamo scrivere dei test. Supponiamo di avere un'interfaccia UserRepository
con un unico metodo per trovare un utente in base al suo identificatore. Abbiamo anche il concetto di codificatore di password per trasformare una password in chiaro in un hash di password. Sia UserRepository
che PasswordEncoder
sono dipendenze (chiamate anche collaboratori) di UserService
iniettate tramite il costruttore. Ecco come appare il nostro codice demo:
Archivio utente
public interface UserRepository { User findById(String id); }
Utente
public class User { private String id; private String passwordHash; private boolean enabled; public User(String id, String passwordHash, boolean enabled) { this.id = id; this.passwordHash = passwordHash; this.enabled = enabled; } ... }
Codificatore di password
public interface PasswordEncoder { String encode(String password); }
Servizio Utente
public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } public boolean isValidUser(String id, String password) { User user = userRepository.findById(id); return isEnabledUser(user) && isValidPassword(user, password); } private boolean isEnabledUser(User user) { return user != null && user.isEnabled(); } private boolean isValidPassword(User user, String password) { String encodedPassword = passwordEncoder.encode(password); return encodedPassword.equals(user.getPasswordHash()); } }
Questo codice di esempio può essere trovato su GitHub, quindi puoi scaricarlo per la revisione insieme a questo articolo.
Applicazione di Mockito
Utilizzando il nostro codice di esempio, vediamo come applicare Mockito e scrivere alcuni test.
Creazione di mock
Con Mockito, creare un mock è facile come chiamare un metodo statico Mockito.mock()
:
import static org.mockito.Mockito.*; ... PasswordEncoder passwordEncoder = mock(PasswordEncoder.class);
Notare l'importazione statica per Mockito. Per il resto di questo articolo, considereremo implicitamente questa importazione aggiunta.
Dopo l'importazione, prendiamo in giro PasswordEncoder
, un'interfaccia. Mockito prende in giro non solo le interfacce, ma anche le classi astratte e le classi concrete non finali. Immediatamente, Mockito non può deridere le classi finali e i metodi finali o statici, ma se ne hai davvero bisogno, Mockito 2 fornisce il plug-in sperimentale MockMaker.
Nota anche che i metodi equals()
e hashCode()
non possono essere presi in giro.
Creare spie
Per creare una spia, devi chiamare il metodo statico spy()
di Mockito e passargli un'istanza da spiare. I metodi di chiamata dell'oggetto restituito chiameranno i metodi reali a meno che tali metodi non siano stub. Queste chiamate vengono registrate e i fatti di queste chiamate possono essere verificati (vedi ulteriore descrizione di verify()
). Facciamo una spia:
DecimalFormat decimalFormat = spy(new DecimalFormat()); assertEquals("42", decimalFormat.format(42L));
Creare una spia non è molto diverso dalla creazione di una simulazione. Inoltre, tutti i metodi Mockito utilizzati per configurare un mock sono applicabili anche alla configurazione di una spia.
Le spie vengono utilizzate raramente rispetto ai mock, ma potresti trovarli utili per testare codice legacy che non può essere rifattorizzato, dove testarlo richiede una presa in giro parziale. In questi casi, puoi semplicemente creare una spia e bloccare alcuni dei suoi metodi per ottenere il comportamento che desideri.
Valori di ritorno predefiniti
La chiamata a mock(PasswordEncoder.class)
restituisce un'istanza di PasswordEncoder
. Possiamo anche chiamare i suoi metodi, ma cosa restituiranno? Per impostazione predefinita, tutti i metodi di un mock restituiscono valori "non inizializzati" o "vuoti", ad esempio zeri per i tipi numerici (sia primitivi che boxed), false per booleani e null per la maggior parte degli altri tipi.
Considera la seguente interfaccia:
interface Demo { int getInt(); Integer getInteger(); double getDouble(); boolean getBoolean(); String getObject(); Collection<String> getCollection(); String[] getArray(); Stream<?> getStream(); Optional<?> getOptional(); }
Consideriamo ora il seguente snippet, che dà un'idea di quali valori predefiniti aspettarsi dai metodi di un mock:
Demo demo = mock(Demo.class); assertEquals(0, demo.getInt()); assertEquals(0, demo.getInteger().intValue()); assertEquals(0d, demo.getDouble(), 0d); assertFalse(demo.getBoolean()); assertNull(demo.getObject()); assertEquals(Collections.emptyList(), demo.getCollection()); assertNull(demo.getArray()); assertEquals(0L, demo.getStream().count()); assertFalse(demo.getOptional().isPresent());
Metodi di stubbing
Scherzi freschi e inalterati sono utili solo in rari casi. Di solito, vogliamo configurare il mock e definire cosa fare quando vengono chiamati metodi specifici del mock. Questo si chiama stubbing .
Mockito offre due modi di stubbing. Il primo modo è " quando viene chiamato questo metodo, allora fai qualcosa". Considera il seguente frammento:
when(passwordEncoder.encode("1")).thenReturn("a");
Si legge quasi come l'inglese: "Quando viene chiamato passwordEncoder.encode(“1”)
, restituisci un a
."
Il secondo modo di stub è più simile a "Fai qualcosa quando il metodo di questo mock viene chiamato con i seguenti argomenti". Questo modo di stub è più difficile da leggere poiché la causa è specificata alla fine. Ritenere:
doReturn("a").when(passwordEncoder).encode("1");
Lo snippet con questo metodo di stubbing sarebbe: "Restituire a
quando il metodo encode()
di passwordEncoder
viene chiamato con un argomento di 1
."
Il primo modo è considerato preferito perché è sicuro per i tipi e perché è più leggibile. Raramente, tuttavia, sei costretto a utilizzare il secondo modo, ad esempio quando si stordisce un metodo reale di una spia perché chiamarlo potrebbe avere effetti collaterali indesiderati.
Esploriamo brevemente i metodi di stubbing forniti da Mockito. Includeremo entrambi i modi di stub nei nostri esempi.
Valori di ritorno
thenReturn
o doReturn()
vengono utilizzati per specificare un valore da restituire al richiamo del metodo.
//”when this method is called, then do something” when(passwordEncoder.encode("1")).thenReturn("a");
o
//”do something when this mock's method is called with the following arguments” doReturn("a").when(passwordEncoder).encode("1");
È inoltre possibile specificare più valori che verranno restituiti come risultati di chiamate di metodo consecutive. L'ultimo valore verrà utilizzato come risultato per tutte le ulteriori chiamate al metodo.
//when when(passwordEncoder.encode("1")).thenReturn("a", "b");
o
//do doReturn("a", "b").when(passwordEncoder).encode("1");
Lo stesso può essere ottenuto con il seguente snippet:
when(passwordEncoder.encode("1")) .thenReturn("a") .thenReturn("b");
Questo modello può essere utilizzato anche con altri metodi di stub per definire i risultati di chiamate consecutive.
Restituzione di risposte personalizzate
then()
, un alias per thenAnswer()
e doAnswer()
ottengono la stessa cosa, ovvero impostare una risposta personalizzata da restituire quando viene chiamato un metodo, in questo modo:
when(passwordEncoder.encode("1")).thenAnswer( invocation -> invocation.getArgument(0) + "!");
o
doAnswer(invocation -> invocation.getArgument(0) + "!") .when(passwordEncoder).encode("1");
L'unico argomento thenAnswer()
è un'implementazione dell'interfaccia Answer
. Ha un unico metodo con un parametro di tipo InvocationOnMock
.
Puoi anche generare un'eccezione come risultato di una chiamata al metodo:
when(passwordEncoder.encode("1")).thenAnswer(invocation -> { throw new IllegalArgumentException(); });
...o chiama il metodo reale di una classe (non applicabile alle interfacce):
Date mock = mock(Date.class); doAnswer(InvocationOnMock::callRealMethod).when(mock).setTime(42); doAnswer(InvocationOnMock::callRealMethod).when(mock).getTime(); mock.setTime(42); assertEquals(42, mock.getTime());
Hai ragione se pensi che sembri ingombrante. Mockito fornisce thenCallRealMethod()
e thenThrow()
per semplificare questo aspetto del test.
Chiamare metodi reali
Come suggerisce il nome, thenCallRealMethod()
e doCallRealMethod()
chiamano il metodo real su un oggetto fittizio:
Date mock = mock(Date.class); when(mock.getTime()).thenCallRealMethod(); doCallRealMethod().when(mock).setTime(42); mock.setTime(42); assertEquals(42, mock.getTime());
Chiamare metodi reali può essere utile su simulazioni parziali, ma assicurati che il metodo chiamato non abbia effetti collaterali indesiderati e non dipenda dallo stato dell'oggetto. Se lo fa, una spia potrebbe essere più adatta di una derisione.
Se crei una simulazione di un'interfaccia e provi a configurare uno stub per chiamare un metodo reale, Mockito genererà un'eccezione con un messaggio molto informativo. Considera il seguente frammento:
when(passwordEncoder.encode("1")).thenCallRealMethod();
Mockito fallirà con il seguente messaggio:
Cannot call abstract real method on java object! Calling real methods is only possible when mocking non abstract method. //correct example: when(mockOfConcreteClass.nonAbstractMethod()).thenCallRealMethod();
Complimenti agli sviluppatori di Mockito per essersi preoccupati abbastanza di fornire descrizioni così complete!
Lanciare eccezioni
thenThrow()
e doThrow()
configurano un metodo simulato per generare un'eccezione:
when(passwordEncoder.encode("1")).thenThrow(new IllegalArgumentException());
o
doThrow(new IllegalArgumentException()).when(passwordEncoder).encode("1");
Mockito garantisce che l'eccezione generata sia valida per quel metodo stubbed specifico e si lamenterà se l'eccezione non è nell'elenco delle eccezioni verificate del metodo. Considera quanto segue:
when(passwordEncoder.encode("1")).thenThrow(new IOException());
Porterà a un errore:
org.mockito.exceptions.base.MockitoException: Checked exception is invalid for this method! Invalid: java.io.IOException
Come puoi vedere, Mockito ha rilevato che encode()
non può generare una IOException
.
Puoi anche passare la classe di un'eccezione invece di passare un'istanza di un'eccezione:
when(passwordEncoder.encode("1")).thenThrow(IllegalArgumentException.class);
o
doThrow(IllegalArgumentException.class).when(passwordEncoder).encode("1");
Detto questo, Mockito non può convalidare una classe di eccezione nello stesso modo in cui convaliderà un'istanza di eccezione, quindi devi essere disciplinato e non passare oggetti di classe illegali. Ad esempio, quanto segue genererà IOException
sebbene encode()
non dovrebbe generare un'eccezione verificata:
when(passwordEncoder.encode("1")).thenThrow(IOException.class); passwordEncoder.encode("1");
Interfacce beffarde con metodi predefiniti
Vale la pena notare che quando si crea un mock per un'interfaccia, Mockito prende in giro tutti i metodi di quell'interfaccia. A partire da Java 8, le interfacce possono contenere metodi predefiniti insieme a metodi astratti. Anche questi metodi vengono presi in giro, quindi è necessario fare attenzione a farli agire come metodi predefiniti.
Considera il seguente esempio:
interface AnInterface { default boolean isTrue() { return true; } } AnInterface mock = mock(AnInterface.class); assertFalse(mock.isTrue());
In questo esempio, assertFalse()
avrà esito positivo. Se non è quello che ti aspettavi, assicurati di aver fatto chiamare Mockito il metodo reale, in questo modo:
AnInterface mock = mock(AnInterface.class); when(mock.isTrue()).thenCallRealMethod(); assertTrue(mock.isTrue());
Corrispondenti argomentativi
Nelle sezioni precedenti, abbiamo configurato i nostri metodi simulati con valori esatti come argomenti. In questi casi, Mockito chiama internamente equals()
per verificare se i valori previsti sono uguali ai valori effettivi.
A volte, però, non conosciamo questi valori in anticipo.
Forse semplicemente non ci interessa il valore effettivo che viene passato come argomento, o forse vogliamo definire una reazione per una gamma più ampia di valori. Tutti questi scenari (e altri) possono essere affrontati con gli abbinatori di argomenti. L'idea è semplice: invece di fornire un valore esatto, fornisci un abbinamento di argomenti a Mockito per confrontare gli argomenti del metodo.
Considera il seguente frammento:
when(passwordEncoder.encode(anyString())).thenReturn("exact"); assertEquals("exact", passwordEncoder.encode("1")); assertEquals("exact", passwordEncoder.encode("abc"));
Puoi vedere che il risultato è lo stesso indipendentemente dal valore che passiamo a encode()
perché abbiamo usato il matcher di argomenti anyString()
in quella prima riga. Se riscriviamo quella riga in un inglese semplice, suonerebbe come "quando al codificatore della password viene chiesto di codificare qualsiasi stringa, quindi restituire la stringa 'esatta'".
Mockito richiede di fornire tutti gli argomenti in base ai corrispondenti o ai valori esatti. Quindi, se un metodo ha più di un argomento e vuoi usare i matcher di argomenti solo per alcuni dei suoi argomenti, dimenticalo. Non puoi scrivere codice come questo:
abstract class AClass { public abstract boolean call(String s, int i); } AClass mock = mock(AClass.class); //This doesn't work. when(mock.call("a", anyInt())).thenReturn(true);
Per correggere l'errore dobbiamo sostituire l'ultima riga per includere l'argomento eq
matcher per a
, come segue:
when(mock.call(eq("a"), anyInt())).thenReturn(true);
Qui abbiamo usato i matcher di argomenti eq()
e anyInt()
, ma ce ne sono molti altri disponibili. Per un elenco completo dei matcher di argomenti, fare riferimento alla documentazione sulla classe org.mockito.ArgumentMatchers
.
È importante notare che non è possibile utilizzare le corrispondenze di argomenti al di fuori della verifica o dello stub. Ad esempio, non puoi avere quanto segue:
//this won't work String orMatcher = or(eq("a"), endsWith("b")); verify(mock).encode(orMatcher);
Mockito rileverà il matcher di argomenti fuori posto e InvalidUseOfMatchersException
. La verifica con i matcher di argomenti dovrebbe essere eseguita in questo modo:
verify(mock).encode(or(eq("a"), endsWith("b")));
Nemmeno i matcher di argomenti possono essere usati come valore di ritorno. Mockito non può restituire anyString()
o any-whatever; un valore esatto è richiesto durante lo stub delle chiamate.
Matcher personalizzati
I matcher personalizzati vengono in soccorso quando devi fornire una logica di corrispondenza che non è già disponibile in Mockito. La decisione di creare un matcher personalizzato non dovrebbe essere presa alla leggera poiché la necessità di abbinare gli argomenti in modo non banale indica o un problema di progettazione o che un test sta diventando troppo complicato.
Pertanto, vale la pena verificare se è possibile semplificare un test utilizzando alcuni dei matcher di argomenti indulgenti come isNull()
e nullable()
prima di scrivere un matcher personalizzato. Se senti ancora il bisogno di scrivere un abbinamento di argomenti, Mockito fornisce una famiglia di metodi per farlo.
Considera il seguente esempio:
FileFilter fileFilter = mock(FileFilter.class); ArgumentMatcher<File> hasLuck = file -> file.getName().endsWith("luck"); when(fileFilter.accept(argThat(hasLuck))).thenReturn(true); assertFalse(fileFilter.accept(new File("/deserve"))); assertTrue(fileFilter.accept(new File("/deserve/luck")));
Qui creiamo il matcher di argomenti hasLuck
e usiamo argThat()
per passare il matcher come argomento a un metodo deriso, bloccandolo in modo che restituisca true
se il nome del file termina con "luck". Puoi trattare ArgumentMatcher
come un'interfaccia funzionale e creare la sua istanza con un lambda (che è ciò che abbiamo fatto nell'esempio). La sintassi meno concisa sarebbe:
ArgumentMatcher<File> hasLuck = new ArgumentMatcher<File>() { @Override public boolean matches(File file) { return file.getName().endsWith("luck"); } };
Se hai bisogno di creare un matcher di argomenti che funzioni con i tipi primitivi, ci sono molti altri metodi per quello in org.mockito.ArgumentMatchers
:
- charThat(ArgumentMatcher<Carattere> abbinamento)
- booleanThat(ArgumentMatcher<Boolean> matcher)
- byteThat(ArgumentMatcher<Byte> matcher)
- shortThat(ArgumentMatcher<Short> matcher)
- intThat(ArgumentMatcher<Integer> matcher)
- longThat(ArgumentMatcher<Long> matcher)
- floatThat(ArgumentMatcher<Float>matcher)
- doubleThat(ArgumentMatcher<Double> matcher)
Combinazione di abbinamenti
Non sempre vale la pena creare un abbinamento di argomenti personalizzato quando una condizione è troppo complicata per essere gestita con abbinamento di base; a volte la combinazione di abbinamenti farà il trucco. Mockito fornisce abbinatori di argomenti per implementare operazioni logiche comuni ('not', 'and', 'or') su abbinatori di argomenti che corrispondono a tipi sia primitivi che non primitivi. Questi abbinatori sono implementati come metodi statici nella classe org.mockito.AdditionalMatchers
.
Considera il seguente esempio:
when(passwordEncoder.encode(or(eq("1"), contains("a")))).thenReturn("ok"); assertEquals("ok", passwordEncoder.encode("1")); assertEquals("ok", passwordEncoder.encode("123abc")); assertNull(passwordEncoder.encode("123"));
Qui abbiamo combinato i risultati di due abbinatori di argomenti: eq("1")
e contains("a")
. L'espressione finale, or(eq("1"), contains("a"))
, può essere interpretata come "la stringa dell'argomento deve essere uguale a "1" o contenere "a".
Nota che ci sono abbinatori meno comuni elencati nella classe org.mockito.AdditionalMatchers
, come geq()
, leq()
, gt()
e lt()
, che sono confronti di valori applicabili per valori primitivi e istanze di java.lang.Comparable
.

Verifica del comportamento
Una volta che è stata utilizzata una simulazione o una spia, possiamo verify
che siano avvenute interazioni specifiche. Letteralmente, stiamo dicendo "Ehi, Mockito, assicurati che questo metodo sia stato chiamato con questi argomenti".
Considera il seguente esempio artificiale:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); when(passwordEncoder.encode("a")).thenReturn("1"); passwordEncoder.encode("a"); verify(passwordEncoder).encode("a");
Qui abbiamo impostato un mock e chiamato il suo metodo encode()
. L'ultima riga verifica che il metodo encode()
del mock sia stato chiamato con il valore dell'argomento specifico a
. Si noti che la verifica di un'invocazione stub è ridondante; lo scopo dello snippet precedente è mostrare l'idea di eseguire la verifica dopo che sono avvenute alcune interazioni.
Se cambiamo l'ultima riga per avere un argomento diverso, ad esempio b
, il test precedente fallirà e Mockito si lamenterà del a
che l'invocazione effettiva ha argomenti diversi ( b
invece dell'a ).
I matcher di argomenti possono essere utilizzati per la verifica proprio come per lo stub:
verify(passwordEncoder).encode(anyString());
Per impostazione predefinita, Mockito verifica che il metodo sia stato chiamato una volta, ma puoi verificare un numero qualsiasi di chiamate:
// verify the exact number of invocations verify(passwordEncoder, times(42)).encode(anyString()); // verify that there was at least one invocation verify(passwordEncoder, atLeastOnce()).encode(anyString()); // verify that there were at least five invocations verify(passwordEncoder, atLeast(5)).encode(anyString()); // verify the maximum number of invocations verify(passwordEncoder, atMost(5)).encode(anyString()); // verify that it was the only invocation and // that there're no more unverified interactions verify(passwordEncoder, only()).encode(anyString()); // verify that there were no invocations verify(passwordEncoder, never()).encode(anyString());
Una caratteristica usata raramente di verify()
è la sua capacità di fallire in un timeout, che è principalmente utile per testare codice simultaneo. Ad esempio, se il nostro codificatore di password viene chiamato in un altro thread contemporaneamente a verify()
, possiamo scrivere un test come segue:
usePasswordEncoderInOtherThread(); verify(passwordEncoder, timeout(500)).encode("a");
Questo test avrà esito positivo se encode()
viene chiamato e terminato entro 500 millisecondi o meno. Se devi attendere l'intero periodo specificato, usa after()
invece di timeout()
:
verify(passwordEncoder, after(500)).encode("a");
Altre modalità di verifica ( times()
, atLeast()
, etc) possono essere combinate con timeout()
e after()
per effettuare test più complicati:
// passes as soon as encode() has been called 3 times within 500 ms verify(passwordEncoder, timeout(500).times(3)).encode("a");
Oltre a times()
, le modalità di verifica supportate includono only()
, atLeast()
e atLeastOnce()
(come alias per atLeast(1)
).
Mockito ti consente anche di verificare l'ordine delle chiamate in un gruppo di mock. Non è una funzionalità da utilizzare molto spesso, ma può essere utile se l'ordine delle invocazioni è importante. Considera il seguente esempio:
PasswordEncoder first = mock(PasswordEncoder.class); PasswordEncoder second = mock(PasswordEncoder.class); // simulate calls first.encode("f1"); second.encode("s1"); first.encode("f2"); // verify call order InOrder inOrder = inOrder(first, second); inOrder.verify(first).encode("f1"); inOrder.verify(second).encode("s1"); inOrder.verify(first).encode("f2");
Se riorganizziamo l'ordine delle chiamate simulate, il test avrà esito negativo con VerificationInOrderFailure
.
L'assenza di invocazioni può essere verificata anche utilizzando verifyZeroInteractions()
. Questo metodo accetta un mock o mocks come argomento e fallirà se viene chiamato qualsiasi metodo del mock(s) passato.
Vale anche la pena menzionare il metodo verifyNoMoreInteractions()
, poiché prende i mock come argomento e può essere utilizzato per verificare che ogni chiamata su quei mock sia stata verificata.
Cattura argomenti
Oltre a verificare che un metodo sia stato chiamato con argomenti specifici, Mockito consente di acquisire tali argomenti in modo da poter eseguire in seguito asserzioni personalizzate su di essi. In altre parole, stai dicendo "Ehi, Mockito, verifica che questo metodo sia stato chiamato e dammi i valori dell'argomento con cui è stato chiamato".
Creiamo una simulazione di PasswordEncoder
, chiamiamo encode()
, catturiamo l'argomento e ne controlliamo il valore:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("password", passwordCaptor.getValue());
Come puoi vedere, passiamo passwordCaptor.capture()
come argomento di encode()
per la verifica; questo crea internamente un matcher di argomenti che salva l'argomento. Quindi recuperiamo il valore acquisito con passwordCaptor.getValue()
e lo ispezioniamo con assertEquals()
.
Se dobbiamo acquisire un argomento su più chiamate, ArgumentCaptor
ti consente di recuperare tutti i valori con getAllValues()
, in questo modo:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password1"); passwordEncoder.encode("password2"); passwordEncoder.encode("password3"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder, times(3)).encode(passwordCaptor.capture()); assertEquals(Arrays.asList("password1", "password2", "password3"), passwordCaptor.getAllValues());
La stessa tecnica può essere utilizzata per acquisire gli argomenti del metodo di ariità variabile (noti anche come varags).
Testare il nostro semplice esempio
Ora che sappiamo molto di più su Mockito, è tempo di tornare alla nostra demo. Scriviamo il test del metodo isValidUser
. Ecco come potrebbe essere:
public class UserServiceTest { private static final String PASSWORD = "password"; private static final User ENABLED_USER = new User("user id", "hash", true); private static final User DISABLED_USER = new User("disabled user id", "disabled user password hash", false); private UserRepository userRepository; private PasswordEncoder passwordEncoder; private UserService userService; @Before public void setup() { userRepository = createUserRepository(); passwordEncoder = createPasswordEncoder(); userService = new UserService(userRepository, passwordEncoder); } @Test public void shouldBeValidForValidCredentials() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), PASSWORD); assertTrue(userIsValid); // userRepository had to be used to find a user with verify(userRepository).findById(ENABLED_USER.getId()); // passwordEncoder had to be used to compute a hash of "password" verify(passwordEncoder).encode(PASSWORD); } @Test public void shouldBeInvalidForInvalidId() { boolean userIsValid = userService.isValidUser("invalid id", PASSWORD); assertFalse(userIsValid); InOrder inOrder = inOrder(userRepository, passwordEncoder); inOrder.verify(userRepository).findById("invalid id"); inOrder.verify(passwordEncoder, never()).encode(anyString()); } @Test public void shouldBeInvalidForInvalidPassword() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), "invalid"); assertFalse(userIsValid); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("invalid", passwordCaptor.getValue()); } @Test public void shouldBeInvalidForDisabledUser() { boolean userIsValid = userService.isValidUser(DISABLED_USER.getId(), PASSWORD); assertFalse(userIsValid); verify(userRepository).findById(DISABLED_USER.getId()); verifyZeroInteractions(passwordEncoder); } private PasswordEncoder createPasswordEncoder() { PasswordEncoder mock = mock(PasswordEncoder.class); when(mock.encode(anyString())).thenReturn("any password hash"); when(mock.encode(PASSWORD)).thenReturn(ENABLED_USER.getPasswordHash()); return mock; } private UserRepository createUserRepository() { UserRepository mock = mock(UserRepository.class); when(mock.findById(ENABLED_USER.getId())).thenReturn(ENABLED_USER); when(mock.findById(DISABLED_USER.getId())).thenReturn(DISABLED_USER); return mock; } }
Immersioni sotto l'API
Mockito fornisce un'API leggibile e conveniente, ma esploriamo alcuni dei suoi meccanismi interni in modo da comprenderne i limiti ed evitare strani errori.
Esaminiamo cosa sta succedendo all'interno di Mockito quando viene eseguito il seguente snippet:
// 1: create PasswordEncoder mock = mock(PasswordEncoder.class); // 2: stub when(mock.encode("a")).thenReturn("1"); // 3: act mock.encode("a"); // 4: verify verify(mock).encode(or(eq("a"), endsWith("b")));
Ovviamente, la prima riga crea un mock. Mockito usa ByteBuddy per creare una sottoclasse della classe data. Il nuovo oggetto classe ha un nome generato come demo.mockito.PasswordEncoder$MockitoMock$1953422997
, il suo equals()
fungerà da controllo dell'identità e hashCode()
restituirà un codice hash di identità. Una volta generata e caricata la classe, la sua istanza viene creata utilizzando Objenesis.
Diamo un'occhiata alla riga successiva:
when(mock.encode("a")).thenReturn("1");
L'ordine è importante: la prima istruzione eseguita qui è mock.encode("a")
, che invocherà encode()
sul mock con un valore di ritorno predefinito null
. Quindi, in realtà, stiamo passando null
come argomento di when()
. A Mockito non interessa quale valore esatto viene passato a when()
perché memorizza le informazioni sull'invocazione di un metodo deriso nel cosiddetto "stubbing in corso" quando quel metodo viene invocato. Successivamente, quando chiamiamo when()
, Mockito estrae quell'oggetto stub in corso e lo restituisce come risultato di when()
. Quindi chiamiamo thenReturn(“1”)
sull'oggetto stub in corso restituito.
La terza riga, mock.encode("a");
è semplice: chiamiamo il metodo stubbed. Internamente, Mockito salva questa invocazione per ulteriori verifiche e restituisce la risposta di invocazione stubbed; nel nostro caso, è la stringa 1
.
Nella quarta riga ( verify(mock).encode(or(eq("a"), endsWith("b")));
), chiediamo a Mockito di verificare che ci sia stata un'invocazione di encode()
con quelli argomenti specifici.
Verify verify()
viene eseguito per primo, il che trasforma lo stato interno di Mockito in modalità di verifica. È importante capire che Mockito mantiene il suo stato in un ThreadLocal
. Ciò rende possibile implementare una bella sintassi ma, d'altra parte, può portare a comportamenti strani se il framework viene utilizzato in modo improprio (se si tenta di utilizzare i matcher di argomenti al di fuori della verifica o dello stubbing, ad esempio).
Quindi, come fa Mockito a creare un or
matcher? Per prima cosa, viene chiamato eq("a")
e un matcher equals
viene aggiunto allo stack dei matchers. In secondo luogo, viene chiamato endsWith("b")
e allo stack viene aggiunto un matcher di endsWith
. Alla fine, viene chiamato or(null, null)
: utilizza i due matcher estratti dallo stack, crea il matcher or
e lo inserisce nello stack. Infine, viene chiamato encode()
. Mockito verifica quindi che il metodo sia stato invocato il numero di volte previsto e con gli argomenti previsti.
Sebbene i matcher di argomenti non possano essere estratti in variabili (perché modificano l'ordine delle chiamate), possono essere estratti in metodi. Ciò preserva l'ordine di chiamata e mantiene lo stack nello stato corretto:
verify(mock).encode(matchCondition()); … String matchCondition() { return or(eq("a"), endsWith("b")); }
Modifica delle risposte predefinite
Nelle sezioni precedenti, abbiamo creato i nostri mock in modo tale che quando vengono chiamati metodi mocked, restituiscono un valore "vuoto". Questo comportamento è configurabile. Puoi anche fornire la tua implementazione di org.mockito.stubbing.Answer
se quelli forniti da Mockito non sono adatti, ma potrebbe essere un'indicazione che qualcosa non va quando gli unit test diventano troppo complicati. Ricorda il principio KISS!
Esploriamo l'offerta di Mockito di risposte predefinite predefinite:
RETURNS_DEFAULTS
è la strategia predefinita; non vale la pena menzionarlo esplicitamente quando si imposta un mock.CALLS_REAL_METHODS
fa in modo che le invocazioni non stubbed richiamino metodi reali.RETURNS_SMART_NULLS
evitaNullPointerException
restituendoSmartNull
invece dinull
quando si utilizza un oggetto restituito da una chiamata al metodo non stubbed. Fallirai comunque con unNullPointerException
, maSmartNull
ti offre una traccia dello stack più piacevole con la riga in cui è stato chiamato il metodo unstubbed. Questo rende utile avereRETURNS_SMART_NULLS
come risposta predefinita in Mockito!RETURNS_MOCKS
prima tenta di restituire valori "vuoti" ordinari, quindi simula, se possibile, enull
in caso contrario. I criteri di vuoto differiscono leggermente da quanto visto in precedenza: invece di restituirenull
per stringhe e array, i mock creati conRETURNS_MOCKS
restituiscono rispettivamente stringhe vuote e array vuoti.RETURNS_SELF
è utile per prendere in giro i costruttori. Con questa impostazione, un mock restituirà un'istanza di se stesso se viene chiamato un metodo che restituisce qualcosa di un tipo uguale alla classe (o una superclasse) della classe simulata.RETURNS_DEEP_STUBS
va più in profondità diRETURNS_MOCKS
e crea mock che sono in grado di restituire mock da mock da mocks, ecc. A differenza diRETURNS_MOCKS
, le regole di vuoto sono predefinite inRETURNS_DEEP_STUBS
, quindi restituiscenull
per stringhe e array:
interface We { Are we(); } interface Are { So are(); } interface So { Deep so(); } interface Deep { boolean deep(); } ... We mock = mock(We.class, Mockito.RETURNS_DEEP_STUBS); when(mock.we().are().so().deep()).thenReturn(true); assertTrue(mock.we().are().so().deep());
Dare un nome a un mock
Mockito ti consente di nominare un mock, una funzionalità utile se hai molti mock in un test e devi distinguerli. Detto questo, la necessità di nominare le prese in giro potrebbe essere un sintomo di un design scadente. Considera quanto segue:
PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class); verify(robustPasswordEncoder).encode(anyString());
Mockito si lamenterà, ma poiché non abbiamo formalmente nominato i mock, non sappiamo quale:
Wanted but not invoked: passwordEncoder.encode(<any string>);
Diamo loro un nome passando una stringa in costruzione:
PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class, "robustPasswordEncoder"); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class, "weakPasswordEncoder"); verify(robustPasswordEncoder).encode(anyString());
Ora il messaggio di errore è più semplice e indica chiaramente robustPasswordEncoder
:
Wanted but not invoked: robustPasswordEncoder.encode(<any string>);
Implementazione di più interfacce fittizie
Sometimes, you may wish to create a mock that implements several interfaces. Mockito is able to do that easily, like so:
PasswordEncoder mock = mock( PasswordEncoder.class, withSettings().extraInterfaces(List.class, Map.class)); assertTrue(mock instanceof List); assertTrue(mock instanceof Map);
Listening Invocations
A mock can be configured to call an invocation listener every time a method of the mock was called. Inside the listener, you can find out whether the invocation produced a value or if an exception was thrown.
InvocationListener invocationListener = new InvocationListener() { @Override public void reportInvocation(MethodInvocationReport report) { if (report.threwException()) { Throwable throwable = report.getThrowable(); // do something with throwable throwable.printStackTrace(); } else { Object returnedValue = report.getReturnedValue(); // do something with returnedValue System.out.println(returnedValue); } } }; PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().invocationListeners(invocationListener)); passwordEncoder.encode("1");
In this example, we're dumping either the returned value or a stack trace to a system output stream. Our implementation does roughly the same as Mockito's org.mockito.internal.debugging.VerboseMockInvocationLogger
(don't use this directly, it's internal stuff). If logging invocations is the only feature you need from the listener, then Mockito provides a cleaner way to express your intent with the verboseLogging()
setting:
PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging());
Take notice, though, that Mockito will call the listeners even when you're stubbing methods. Considera il seguente esempio:
PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging()); // listeners are called upon encode() invocation when(passwordEncoder.encode("1")).thenReturn("encoded1"); passwordEncoder.encode("1"); passwordEncoder.encode("2");
This snippet will produce an output similar to the following:
############ Logging method invocation #1 on mock/spy ######## passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) has returned: "null" ############ Logging method invocation #2 on mock/spy ######## stubbed: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:89) has returned: "encoded1" (java.lang.String) ############ Logging method invocation #3 on mock/spy ######## passwordEncoder.encode("2"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:90) has returned: "null"
Note that the first logged invocation corresponds to calling encode()
while stubbing it. It's the next invocation that corresponds to calling the stubbed method.
Altre impostazioni
Mockito offers a few more settings that let you do the following:
- Enable mock serialization by using
withSettings().serializable()
. - Turn off recording of method invocations to save memory (this will make verification impossible) by using
withSettings().stubOnly()
. - Use the constructor of a mock when creating its instance by using
withSettings().useConstructor()
. When mocking inner non-static classes, add anouterInstance()
setting, like so:withSettings().useConstructor().outerInstance(outerObject)
.
If you need to create a spy with custom settings (such as a custom name), there's a spiedInstance()
setting, so that Mockito will create a spy on the instance you provide, like so:
UserService userService = new UserService( mock(UserRepository.class), mock(PasswordEncoder.class)); UserService userServiceMock = mock( UserService.class, withSettings().spiedInstance(userService).name("coolService"));
When a spied instance is specified, Mockito will create a new instance and populate its non-static fields with values from the original object. That's why it's important to use the returned instance: Only its method calls can be stubbed and verified.
Note that, when you create a spy, you're basically creating a mock that calls real methods:
// creating a spy this way... spy(userService); // ... is a shorthand for mock(UserService.class, withSettings() .spiedInstance(userService) .defaultAnswer(CALLS_REAL_METHODS));
When Mockito Tastes Bad
It's our bad habits that make our tests complex and unmaintainable, not Mockito. For example, you may feel the need to mock everything. This kind of thinking leads to testing mocks instead of production code. Mocking third-party APIs can also be dangerous due to potential changes in that API that can break the tests.
Though bad taste is a matter of perception, Mockito provides a few controversial features that can make your tests less maintainable. Sometimes stubbing isn't trivial, or an abuse of dependency injection can make recreating mocks for each test difficult, unreasonable or inefficient.
Clearing Invocations
Mockito allows for clearing invocations for mocks while preserving stubbing, like so:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); UserRepository userRepository = mock(UserRepository.class); // use mocks passwordEncoder.encode(null); userRepository.findById(null); // clear clearInvocations(passwordEncoder, userRepository); // succeeds because invocations were cleared verifyZeroInteractions(passwordEncoder, userRepository);
Resort to clearing invocations only if recreating a mock would lead to significant overhead or if a configured mock is provided by a dependency injection framework and stubbing is non-trivial.
Resetting a Mock
Resetting a mock with reset()
is another controversial feature and should be used in extremely rare cases, like when a mock is injected by a container and you can't recreate it for each test.
Overusing Verify
Another bad habit is trying to replace every assert with Mockito's verify()
. It's important to clearly understand what is being tested: interactions between collaborators can be checked with verify()
, while confirming the observable results of an executed action is done with asserts.
Mockito Is about Frame of Mind
Using Mockito is not just a matter of adding another dependency, it requires changing how you think about your unit tests while removing a lot of boilerplate.
With multiple mock interfaces, listening invocations, matchers and argument captors, we've seen how Mockito makes your tests cleaner and easier to understand, but like any tool, it must be used appropriately to be useful. Now armed with the knowledge of Mockito's inner workings, you can take your unit testing to the next level.