Mocking in Java con Mockito
Sostituisci le dipendenze nei test Java con Mockito: mock, when/thenReturn, verify e argument captor.
Un unit test dovrebbe esercitare una sola classe in isolamento. Ma le classi reali si appoggiano a collaboratori — un database, un gateway di pagamento, un mittente di email — che sono lenti, inaffidabili o hanno effetti collaterali indesiderati in un test. Mockito è la libreria Java più utilizzata per sostituire quei collaboratori con mock: oggetti sostitutivi che si programmano per restituire risposte predefinite e che poi si interrogano su come sono stati chiamati. Questo capitolo mostra le API di Mockito che utilizzerai ogni giorno e dimostra il concetto di base con un programma JDK puro che puoi eseguire direttamente qui.
Questo capitolo presuppone che tu conosca già le basi del testing trattate in Introduzione a JUnit 5 e Asserzioni JUnit. Mockito completa JUnit — JUnit esegue il test e verifica i valori, mentre Mockito fornisce i collaboratori fittizi.
Perché usare i mock
La classe sotto test (il system under test, o SUT) di solito riceve i suoi collaboratori attraverso il costruttore — è questo il vantaggio della dependency injection. In un test si passa un collaboratore fittizio al posto di quello reale. Un buon finto collaboratore svolge due compiti:
- Stubbing — restituisce il valore necessario per lo scenario di test (
charge(...)restituiscetrue, o lancia un'eccezione), in modo da poter guidare il SUT lungo un percorso specifico senza una reale chiamata di rete. - Verifica — registra ogni chiamata ricevuta, così il test può in seguito affermare che il SUT lo ha chiamato nel modo giusto, il numero corretto di volte e con gli argomenti corretti.
Mockito genera un tale oggetto fittizio per qualsiasi interfaccia o classe non-final a runtime, così non è necessario scriverne uno a mano. Ma capire cosa genera rende l'API ovvia.
Creare mock e stubbing dei valori di ritorno
Mockito.mock(Type.class) produce un mock. Per impostazione predefinita ogni metodo restituisce un valore "gentile" vuoto — null per gli oggetti, false per i booleani, 0 per i numeri. Poi si sovrascrivono i metodi rilevanti con when(...).thenReturn(...).
import static org.mockito.Mockito.*;
PaymentGateway gateway = mock(PaymentGateway.class);
// Stub: when charge is called with these args, return true.
when(gateway.charge("acct-7", 1999)).thenReturn(true);
// Stub a method to throw, to test error handling.
when(gateway.charge("acct-x", 1)).thenThrow(new GatewayException("down"));Per i metodi void l'ordine si inverte: doThrow(...).when(mock).method(). Gli stub possono anche essere resi più flessibili con argument matcher come anyString() e anyInt(), in modo che si attivino per qualsiasi chiamata e non solo per un preciso set di argomenti.
Verifica delle interazioni
Dopo l'esecuzione del SUT, verify(...) afferma come è stato utilizzato il mock. È così che si testano gli effetti collaterali — un'email che avrebbe dovuto essere inviata, una riga che avrebbe dovuto essere salvata — senza ispezionare il sistema reale.
verify(gateway).charge("acct-7", 1999); // called exactly once (default)
verify(gateway, times(2)).charge(anyString(), anyInt());
verify(gateway, never()).refund(anyString()); // must NOT have been called
verifyNoMoreInteractions(gateway); // nothing else happenedLe modalità di verifica più comuni:
| Modalità | Significato |
|---|---|
times(n) | Chiamato esattamente n volte |
never() | Equivalente a times(0) |
atLeastOnce() / atLeast(n) | Chiamato almeno una volta / n volte |
atMost(n) | Chiamato al massimo n volte |
only() | È stato l'unico metodo chiamato sul mock |
Cattura degli argomenti
Quando occorre ispezionare cosa è stato passato — non solo che una chiamata sia avvenuta — si usa un ArgumentCaptor. Cattura l'argomento effettivo in modo da poter asserire sui suoi campi, il che è prezioso quando il SUT costruisce un oggetto prima di passarlo avanti.
ArgumentCaptor<Order> captor = ArgumentCaptor.forClass(Order.class);
verify(repository).save(captor.capture());
Order saved = captor.getValue();
assertEquals("acct-7", saved.account());
assertEquals(1999, saved.amountCents());@Mock, @InjectMocks e spy
Nelle classi di test reali raramente si chiama mock() manualmente. Le annotazioni gestiscono tutto: @Mock dichiara un campo mock, @InjectMocks costruisce il SUT e inietta i mock nel suo costruttore, e @ExtendWith(MockitoExtension.class) (JUnit 5) attiva l'elaborazione.
@ExtendWith(MockitoExtension.class)
class CheckoutServiceTest {
@Mock PaymentGateway gateway;
@InjectMocks CheckoutService service; // gets the mock injected
@Test
void paysWhenGatewayApproves() {
when(gateway.charge("acct-7", 1999)).thenReturn(true);
assertEquals("PAID", service.checkout("acct-7", 1999));
verify(gateway).charge("acct-7", 1999);
}
}Uno spy (spy(realObject)) è la via di mezzo: avvolge un oggetto reale ed esegue i metodi reali a meno che non siano stubbed — utile per il mocking parziale di codice legacy.
final, metodi final, metodi static o metodi private. Se devi fare mock di una classe final, abilita il MockMaker mockito-inline; altrimenti refactorizza verso un'interfaccia.Quando non usare i mock
I mock sono potenti, ma un eccesso di mocking produce test che passano mentre il codice reale è rotto. Ricorri a un mock solo quando il collaboratore reale è lento, non deterministico, ha effetti collaterali o non è ancora costruito. Non fare mock di value object, della classe sotto test stessa o di tipi che non possiedi (avvolgi un'API di terze parti nella tua interfaccia e fa' mock di quella). Quando il collaboratore è economico e puro — una semplice calcolatrice, una lista in memoria — usa quello reale e asserisci direttamente sul suo risultato.
Un esempio pratico: un mock costruito a mano
Mockito non è disponibile nel classpath di questa pagina, quindi il programma eseguibile qui sotto costruisce il mock a mano — una piccola classe che implementa l'interfaccia della dipendenza, che contiene un valore di ritorno stubbed e registra ogni chiamata. È esattamente la meccanica che Mockito genera per te a runtime, quindi leggerla ti dice esattamente cosa fanno when/thenReturn e verify sotto il cofano.
Cosa ricavare dall'esecuzione:
- Il
stubbedResult = truediMockGatewayè la forma scritta a mano diwhen(gateway.charge(...)).thenReturn(true); poiché lo stub ha restituitotrue, il SUT ha stampatoresult : PAIDsenza che avvenisse alcun pagamento reale. invocationCount == 1che stampatrueè esattamente ciò cheverify(gateway).charge(...)controlla — il mock ha contato di essere stato chiamato una volta, ed è così che Mockito trasforma "questa interazione è avvenuta?" in un'asserzione pass/fail.- La lista
callsha catturatocharge(acct-7, 1999), l'idea di argument-capture dietroArgumentCaptor: un mock ricorda non solo che è stato chiamato ma con cosa, così il test può asserire sugli argomenti effettivi. - Ricreare il mock con
stubbedResult = falseha guidato il SUT lungo l'altro ramo e stampatodeclined result : DECLINED, mostrando come un singolo finto collaboratore permetta di simulare ogni scenario che il collaboratore reale potrebbe produrre. - La clausola di guardia ha restituito
INVALIDprima di raggiungere il gateway, quindiinvocationCount == 0ha stampatotrue— la prova eseguibile diverify(gateway, never()).charge(...), che asserisce che una dipendenza non è stata deliberatamente toccata.