W3docs

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(...) restituisce true, 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 happened

Le 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.

Attenzione
Mockito può fare mock solo di ciò che è sovrascrivibile. Per impostazione predefinita non può fare mock di classi 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.

java— editable, runs on the server

Cosa ricavare dall'esecuzione:

  • Il stubbedResult = true di MockGateway è la forma scritta a mano di when(gateway.charge(...)).thenReturn(true); poiché lo stub ha restituito true, il SUT ha stampato result : PAID senza che avvenisse alcun pagamento reale.
  • invocationCount == 1 che stampa true è esattamente ciò che verify(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 calls ha catturato charge(acct-7, 1999), l'idea di argument-capture dietro ArgumentCaptor: un mock ricorda non solo che è stato chiamato ma con cosa, così il test può asserire sugli argomenti effettivi.
  • Ricreare il mock con stubbedResult = false ha guidato il SUT lungo l'altro ramo e stampato declined 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 INVALID prima di raggiungere il gateway, quindi invocationCount == 0 ha stampato true — la prova eseguibile di verify(gateway, never()).charge(...), che asserisce che una dipendenza non è stata deliberatamente toccata.

Esercitazione

Pratica
In un unit test basato su Mockito, qual è lo scopo di una chiamata come verify(gateway, never()).charge(anyString(), anyInt())?
In un unit test basato su Mockito, qual è lo scopo di una chiamata come verify(gateway, never()).charge(anyString(), anyInt())?
Was this page helpful?