Come funziona l'invocazione mockito when ()?


111

Data la seguente dichiarazione Mockito:

when(mock.method()).thenReturn(someValue);

Come fa Mockito a creare qualcosa di proxy per un mock, dato che l'istruzione mock.method () passerà il valore di ritorno a when ()? Immagino che questo utilizzi alcune cose CGLib, ma sarei interessato a sapere come questo è tecnicamente fatto.

Risposte:


118

La risposta breve è che nel tuo esempio, il risultato di mock.method()sarà un valore vuoto appropriato per il tipo; mockito utilizza l'indirizzamento tramite proxy, intercettazione del metodo e un'istanza condivisa della MockingProgressclasse per determinare se un'invocazione di un metodo su un mock è per lo stubbing o la riproduzione di un comportamento stubbed esistente piuttosto che passare informazioni sullo stubbing tramite il valore di ritorno di un metodo deriso.

Una mini-analisi in un paio di minuti guardando il codice mockito è la seguente. Nota, questa è una descrizione molto approssimativa: ci sono molti dettagli in gioco qui. Ti suggerisco di controllare tu stesso la fonte su GitHub .

Innanzitutto, quando deridi una classe usando il mockmetodo della Mockitoclasse, questo è essenzialmente ciò che accade:

  1. Mockito.mockdelega a org.mockito.internal.MockitoCore.mock, passando le impostazioni fittizie predefinite come parametro.
  2. MockitoCore.mockdelegati a org.mockito.internal.util.MockUtil.createMock
  3. La MockUtilclasse utilizza la ClassPathLoaderclasse per ottenere un'istanza MockMakerda utilizzare per creare il mock. Per impostazione predefinita, viene utilizzata la classe CgLibMockMaker .
  4. CgLibMockMakerutilizza una classe presa in prestito da JMock, ClassImposterizerche gestisce la creazione del mock. I pezzi chiave della "magia mockito" utilizzati sono MethodInterceptorusati per creare il mock: il mockito MethodInterceptorFiltere una catena di istanze MockHandler, inclusa un'istanza di MockHandlerImpl . L'intercettore del metodo passa le invocazioni all'istanza MockHandlerImpl, che implementa la logica di business che dovrebbe essere applicata quando un metodo viene invocato su un mock (cioè, cercando per vedere se una risposta è già registrata, determinando se l'invocazione rappresenta un nuovo stub, ecc. Lo stato predefinito è che se uno stub non è già registrato per il metodo richiamato, viene restituito un valore vuoto appropriato per il tipo .

Ora, diamo un'occhiata al codice nel tuo esempio:

when(mock.method()).thenReturn(someValue)

Ecco l'ordine in cui verrà eseguito questo codice:

  1. mock.method()
  2. when(<result of step 1>)
  3. <result of step 2>.thenReturn

La chiave per capire cosa sta succedendo è cosa succede quando viene richiamato il metodo sul mock: all'intercettore del metodo vengono passate le informazioni sulla chiamata del metodo e le delega alla sua catena di MockHandleristanze, che alla fine delegano a MockHandlerImpl#handle. Durante MockHandlerImpl#handle, il gestore fittizio crea un'istanza di OngoingStubbingImple la passa MockingProgressall'istanza condivisa .

Quando il whenmetodo viene invocato dopo l'invocazione di method(), delega a MockitoCore.when, che chiama il stub()metodo della stessa classe. Questo metodo decomprime lo stubbing in corso dall'istanza condivisa in MockingProgresscui è stata method()scritta l'invocazione derisa e la restituisce. Quindi il thenReturnmetodo viene quindi chiamato OngoingStubbingsull'istanza.


1
Grazie per la risposta dettagliata. Un'altra domanda - dici che "quando il metodo viene invocato dopo l'invocazione di method ()" - come fa a sapere che l'invocazione di when () è la successiva invocazione (o avvolge) l'invocazione di method ()? Spero che abbia un senso.
marchaos

@marchaos Non lo sa. Con la when(mock.method()).thenXyz(...)sintassi, mock.method()viene eseguito in modalità "replay", non in modalità "stubbing". Normalmente, questa esecuzione di mock.method()non ha effetto, quindi in seguito quando thenXyz(...)( thenReturn,thenThrow , thenAnswer, ecc) viene eseguito, va in "spegnendo" modalità e quindi registra il risultato desiderato per quel metodo di chiamata.
Rogério

1
Rogerio, in realtà è un po 'più sottile di così: mockito non ha modalità di stubbing e replay esplicite. Modificherò la mia risposta più tardi in modo che sia più chiara.
Paul Morie

Sommerei che in breve, è più facile intercettare una chiamata di metodo all'interno di un altro metodo con CGLIB o Javassist che intercettare, ad esempio, l'operatore "if".
Infeligo

Non ho ancora massaggiato la mia descrizione qui, ma non me ne sono nemmeno dimenticato. FYI.
Paul Morie

33

La risposta breve è, dietro le quinte, Mockito utilizza una sorta di variabili / memoria globali per salvare le informazioni sui passaggi di creazione dello stub del metodo (invocazione di method (), when (), thenReturn () nel tuo esempio), in modo che alla fine possa costruire una mappa su cosa dovrebbe essere restituito quando ciò che viene chiamato su quale parametro.

Ho trovato questo articolo molto utile: Spiegazione di come funzionano i Mock Frameworks basati su proxy ( http://blog.rseiler.at/2014/06/explanation-how-proxy-based-mock.html ). L'autore ha implementato un framework dimostrativo Mocking, che ho trovato un'ottima risorsa per le persone che vogliono capire come funzionano questi framework Mocking.

A mio parere, è un utilizzo tipico di Anti-Pattern. Normalmente dovremmo evitare "effetti collaterali" quando implementiamo un metodo, il che significa che il metodo dovrebbe accettare l'input, fare dei calcoli e restituire il risultato - nient'altro è cambiato oltre a quello. Ma Mockito viola quella regola di proposito. I suoi metodi memorizzano un mucchio di informazioni oltre a restituire il risultato: Mockito.anyString (), mockInstance.method (), when (), thenReturn, hanno tutti uno speciale "effetto collaterale". Questo è anche il motivo per cui il framework sembra una magia a prima vista: di solito non scriviamo codice in questo modo. Tuttavia, nel caso del framework beffardo, questo design anti-pattern è un ottimo design, poiché porta a API molto semplici.


4
Eccellente collegamento. Il genio dietro questo è: l'API molto semplice che rende il tutto molto bello. Un'altra grande decisione è che il metodo when () utilizza generici in modo che il metodo thenReturn () sia indipendente dai tipi.
David Tonhofer

Considero questa la risposta migliore. A differenza dell'altra risposta, spiega chiaramente i concetti del processo di derisione invece del flusso di controllo attraverso un codice concreto. Sono d'accordo, ottimo collegamento.
mihca
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.