Come funzionano gli abbinatori Mockito?


122

Matchers argomento Mockito (quali any, argThat, eq, same, e ArgumentCaptor.capture()) si comportano in modo molto diverso da matchers Hamcrest.

  • I matcher Mockito spesso causano InvalidUseOfMatchersException, anche nel codice che viene eseguito molto tempo dopo che sono stati utilizzati i matcher.

  • I matcher Mockito sono soggetti a regole strane, come richiedere l'uso di matcher Mockito per tutti gli argomenti solo se un argomento in un dato metodo usa un matcher.

  • Gli abbinamenti Mockito possono causare NullPointerException quando si sovrascrive Answers o quando si usa (Integer) any()ecc.

  • Il refactoring del codice con i matcher Mockito in determinati modi può produrre eccezioni e comportamenti imprevisti e potrebbe non riuscire completamente.

Perché i matcher Mockito sono progettati in questo modo e come vengono implementati?

Risposte:


236

I matcher mockito sono metodi statici e chiamate a tali metodi, che sostituiscono gli argomenti durante le chiamate a whene verify.

I matcher di Hamcrest (versione archiviata) (o matcher in stile Hamcrest) sono istanze di oggetti generiche e senza stato che implementano Matcher<T>ed espongono un metodo matches(T)che restituisce true se l'oggetto corrisponde ai criteri del Matcher. Sono intesi per essere privi di effetti collaterali e sono generalmente utilizzati in affermazioni come quella di seguito.

/* Mockito */  verify(foo).setPowerLevel(gt(9000));
/* Hamcrest */ assertThat(foo.getPowerLevel(), is(greaterThan(9000)));

Esistono matcher Mockito, separati dai matcher in stile Hamcrest, in modo che le descrizioni delle espressioni corrispondenti si adattino direttamente alle invocazioni dei metodi : i matcher Mockito restituiscono Tdove i metodi Matcher Hamcrest restituiscono oggetti Matcher (di tipo Matcher<T>).

Matchers Mockito sono invocati attraverso metodi statici come eq, any, gte startsWithsu org.mockito.Matcherse org.mockito.AdditionalMatchers. Ci sono anche adattatori, che sono cambiati nelle versioni di Mockito:

  • Per Mockito 1.x, Matchersalcune chiamate (come intThato argThat) sono matcher Mockito che accettano direttamente i matcher Hamcrest come parametri. ArgumentMatcher<T>esteso org.hamcrest.Matcher<T>, che è stato utilizzato nella rappresentazione interna di Hamcrest ed era una classe base del matcher Hamcrest invece di qualsiasi tipo di matcher Mockito.
  • Per Mockito 2.0+, Mockito non ha più una dipendenza diretta da Hamcrest. Matcherschiama phrased as intThato argThatwrap ArgumentMatcher<T>oggetti che non vengono più implementati org.hamcrest.Matcher<T>ma utilizzati in modi simili. Gli adattatori Hamcrest come argThate intThatsono ancora disponibili, ma sono stati MockitoHamcrestinvece spostati .

Indipendentemente dal fatto che gli abbinatori siano Hamcrest o semplicemente in stile Hamcrest, possono essere adattati in questo modo:

/* Mockito matcher intThat adapting Hamcrest-style matcher is(greaterThan(...)) */
verify(foo).setPowerLevel(intThat(is(greaterThan(9000))));

Nella dichiarazione precedente: foo.setPowerLevelè un metodo che accetta un file int. is(greaterThan(9000))restituisce a Matcher<Integer>, che non funzionerebbe come setPowerLevelargomento. Il Mockito Matcher intThatavvolge quel Matcher in stile Hamcrest e restituisce un in intmodo che possa apparire come argomento; I matcher mockito come gt(9000)avvolgono l'intera espressione in una singola chiamata, come nella prima riga del codice di esempio.

Cosa fanno / restituiscono gli abbinatori

when(foo.quux(3, 5)).thenReturn(true);

Quando non si utilizzano corrispondenze di argomenti, Mockito registra i valori degli argomenti e li confronta con i loro equalsmetodi.

when(foo.quux(eq(3), eq(5))).thenReturn(true);    // same as above
when(foo.quux(anyInt(), gt(5))).thenReturn(true); // this one's different

Quando chiami un matcher come anyo gt(maggiore di), Mockito memorizza un oggetto matcher che fa sì che Mockito salti quel controllo di uguaglianza e applichi la tua corrispondenza scelta. Nel caso argumentCaptor.capture()memorizza un matcher che salva il suo argomento invece per un'ispezione successiva.

I matcher restituiscono valori fittizi come zero, raccolte vuote o null. Mockito cerca di restituire un valore fittizio sicuro e appropriato, come 0 per anyInt()o any(Integer.class)o vuoto List<String>per anyListOf(String.class). A causa della cancellazione del tipo, tuttavia, a Mockito mancano le informazioni sul tipo per restituire qualsiasi valore tranne che nullper any()o argThat(...), che può causare un'eccezione NullPointerException se si tenta di "unbox automatico" un nullvalore primitivo.

Ai matcher piacciono eqe gtaccettano i valori dei parametri; idealmente, questi valori dovrebbero essere calcolati prima dell'inizio dello stubbing / verifica. Chiamare una finta mentre si prende in giro un'altra chiamata può interferire con lo stubbing.

I metodi Matcher non possono essere utilizzati come valori di ritorno; non c'è modo di formulare thenReturn(anyInt())o thenReturn(any(Foo.class))in Mockito, per esempio. Mockito deve sapere esattamente quale istanza restituire nelle chiamate stubbing e non sceglierà un valore di ritorno arbitrario per te.

Dettagli di implementazione

I matcher vengono memorizzati (come abbinamenti di oggetti in stile Hamcrest) in uno stack contenuto in una classe chiamata ArgumentMatcherStorage . MockitoCore e Matchers possiedono ciascuno un'istanza ThreadSafeMockingProgress , che contiene staticamente un ThreadLocal che contiene istanze MockingProgress. È questo MockingProgressImpl che contiene un ArgumentMatcherStorageImpl concreto . Di conseguenza, lo stato mock e matcher è statico ma con ambito thread in modo coerente tra le classi Mockito e Matchers.

La maggior parte delle chiamate matcher solo aggiungere questo stack, con un'eccezione per matchers come and, orenot . Ciò corrisponde perfettamente (e si basa su) l' ordine di valutazione di Java , che valuta gli argomenti da sinistra a destra prima di invocare un metodo:

when(foo.quux(anyInt(), and(gt(10), lt(20)))).thenReturn(true);
[6]      [5]  [1]       [4] [2]     [3]

Questo sarà:

  1. Aggiungi anyInt()alla pila.
  2. Aggiungi gt(10)alla pila.
  3. Aggiungi lt(20)alla pila.
  4. Rimuovere gt(10)e lt(20)ed aggiungere and(gt(10), lt(20)).
  5. Call foo.quux(0, 0), che (se non diversamente bloccato) restituisce il valore predefinito false. Internamente Mockito segna quux(int, int)come la chiamata più recente.
  6. Call when(false), che scarta il suo argomento e si prepara allo stub del metodo quux(int, int)identificato in 5. Gli unici due stati validi sono con lunghezza dello stack 0 (uguaglianza) o 2 (matchers), e ci sono due matcher nello stack (passaggi 1 e 4), quindi Mockito stub il metodo con un any()matcher per il suo primo argomento e and(gt(10), lt(20))per il secondo argomento e cancella lo stack.

Questo dimostra alcune regole:

  • Mockito non riesce a distinguere tra quux(anyInt(), 0)e quux(0, anyInt()). Entrambi sembrano una chiamata a quux(0, 0)con un int matcher in pila. Di conseguenza, se usi un matcher, devi abbinare tutti gli argomenti.

  • L'ordine delle chiamate non è solo importante, è ciò che fa funzionare tutto questo . L'estrazione di abbinamenti alle variabili in genere non funziona, perché di solito cambia l'ordine di chiamata. L'estrazione di abbinamenti ai metodi, tuttavia, funziona alla grande.

    int between10And20 = and(gt(10), lt(20));
    /* BAD */ when(foo.quux(anyInt(), between10And20)).thenReturn(true);
    // Mockito sees the stack as the opposite: and(gt(10), lt(20)), anyInt().
    
    public static int anyIntBetween10And20() { return and(gt(10), lt(20)); }
    /* OK */  when(foo.quux(anyInt(), anyIntBetween10And20())).thenReturn(true);
    // The helper method calls the matcher methods in the right order.
  • Lo stack cambia abbastanza spesso che Mockito non può controllarlo con molta attenzione. Può controllare lo stack solo quando interagisci con Mockito o con un mock, e deve accettare matcher senza sapere se vengono usati immediatamente o abbandonati accidentalmente. In teoria, lo stack dovrebbe essere sempre vuoto al di fuori di una chiamata a wheno verify, ma Mockito non può controllarlo automaticamente. Puoi controllare manualmente con Mockito.validateMockitoUsage().

  • In una chiamata a when, Mockito chiama effettivamente il metodo in questione, che genererà un'eccezione se hai bloccato il metodo per generare un'eccezione (o richiedi valori diversi da zero o non nulli). doReturne doAnswer(ecc.) non invocano il metodo effettivo e sono spesso un'utile alternativa.

  • Se avessi chiamato un metodo fittizio nel mezzo dello stubbing (ad esempio per calcolare una risposta per un eqmatcher), Mockito avrebbe invece verificato la lunghezza dello stack rispetto a quella chiamata e probabilmente avrebbe fallito.

  • Se provi a fare qualcosa di sbagliato, come lo stubbing / la verifica di un metodo finale , Mockito chiamerà il metodo reale e lascerà anche gli abbinatori extra in pila . La finalchiamata al metodo potrebbe non generare un'eccezione, ma potresti ottenere un'eccezione InvalidUseOfMatchersException dai matcher vaganti quando interagisci con una simulazione.

Problemi comuni

  • InvalidUseOfMatchersException :

    • Verifica che ogni singolo argomento abbia esattamente una chiamata matcher, se usi tutti i matcher, e che non hai usato un matcher al di fuori di una chiamata whenor verify. I matcher non dovrebbero mai essere usati come valori di ritorno stubbed o campi / variabili.

    • Verifica di non chiamare una finta come parte del fornire un argomento di corrispondenza.

    • Verifica di non provare a bloccare / verificare un metodo finale con un matcher. È un ottimo modo per lasciare un matcher in pila e, a meno che il tuo metodo finale non generi un'eccezione, questa potrebbe essere l'unica volta in cui ti rendi conto che il metodo che stai prendendo in giro è definitivo.

  • NullPointerException con argomenti primitivi: (Integer) any() restituisce null mentre any(Integer.class)restituisce 0; questo può causare un NullPointerExceptionse ti aspetti un intinvece di un numero intero. In ogni caso, preferisci anyInt(), che restituirà zero e salterà anche il passaggio dell'auto-boxing.

  • NullPointerException o altre eccezioni: le chiamate a when(foo.bar(any())).thenReturn(baz)verranno effettivamente chiamate foo.bar(null) , cosa che potresti aver bloccato per generare un'eccezione quando ricevi un argomento null. Il passaggio a doReturn(baz).when(foo).bar(any()) salta il comportamento stub .

Risoluzione dei problemi generali

  • Usa MockitoJUnitRunner o chiama esplicitamente il validateMockitoUsagetuo metodo tearDowno @After(cosa che il runner farebbe automaticamente per te). Questo ti aiuterà a determinare se hai abusato dei matcher.

  • Ai fini del debug, aggiungi direttamente le chiamate a validateMockitoUsagenel codice. Questo lancerà se hai qualcosa in pila, che è un buon avvertimento di un cattivo sintomo.


2
Grazie per questo articolo. Un'eccezione NullPointerException con il formato when / thenReturn mi stava causando problemi, finché non l'ho modificata in doReturn / when.
yngwietiger

11

Solo una piccola aggiunta all'eccellente risposta di Jeff Bowman, poiché ho trovato questa domanda durante la ricerca di una soluzione a uno dei miei problemi:

Se una chiamata a un metodo corrisponde a più whenchiamate addestrate di un mock , l'ordine delle whenchiamate è importante e dovrebbe essere dalla più ampia alla più specifica. Partendo da uno degli esempi di Jeff:

when(foo.quux(anyInt(), anyInt())).thenReturn(true);
when(foo.quux(anyInt(), eq(5))).thenReturn(false);

è l'ordine che garantisce il risultato (probabilmente) desiderato:

foo.quux(3 /*any int*/, 8 /*any other int than 5*/) //returns true
foo.quux(2 /*any int*/, 5) //returns false

Se inverti le chiamate quando, il risultato sarà sempre true.


2
Sebbene questa sia un'informazione utile, riguarda lo stubbing , non i matchers , quindi potrebbe non avere senso su questa domanda. L'ordine è importante, ma solo in quanto vince l'ultima catena di corrispondenza definita : ciò significa che gli stub coesistenti sono spesso dichiarati più specifici per meno, ma in alcuni casi potresti volere una sostituzione molto ampia di comportamenti specificamente presi in giro in un singolo caso di test , a quel punto potrebbe essere necessaria una definizione ampia.
Jeff Bowman

1
@ JeffBowman Ho pensato che avesse senso su questa domanda poiché la domanda riguarda i matcher mockito e i matcher possono essere usati durante lo stubbing (come nella maggior parte dei tuoi esempi). Poiché la ricerca di una spiegazione su Google mi ha portato a questa domanda, penso che sia utile avere queste informazioni qui.
tibt del
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.