Wie funktionieren Mockito Matcher?

122

Mockito Argument Matcher (wie any, argThat, eq, same, und ArgumentCaptor.capture()) verhalten sich sehr unterschiedlich aus hamcrest Matcher.

  • Mockito-Matcher verursachen häufig eine InvalidUseOfMatchersException, selbst in Code, der lange nach der Verwendung von Matchern ausgeführt wird.

  • Mockito-Matcher unterliegen seltsamen Regeln, z. B. müssen nur Mockito-Matcher für alle Argumente verwendet werden, wenn ein Argument in einer bestimmten Methode einen Matcher verwendet.

  • Mockito-Matcher können beim Überschreiben von Answers oder bei der Verwendung von (Integer) any()etc. eine NullPointerException verursachen .

  • Das Refactoring von Code mit Mockito-Matchern auf bestimmte Weise kann zu Ausnahmen und unerwartetem Verhalten führen und vollständig fehlschlagen.

Warum sind Mockito-Matcher so konzipiert und wie werden sie implementiert?

Jeff Bowman
quelle

Antworten:

236

Mockito-Matcher sind statische Methoden und Aufrufe dieser Methoden, die bei Aufrufen von und für Argumente stehen .whenverify

Hamcrest-Matcher (archivierte Version) (oder Hamcrest-Matcher) sind zustandslose Allzweckobjektinstanzen, Matcher<T>die eine Methode implementieren und matches(T)verfügbar machen, die true zurückgibt, wenn das Objekt den Kriterien des Matchers entspricht. Sie sollen frei von Nebenwirkungen sein und werden im Allgemeinen in Behauptungen wie der folgenden verwendet.

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

Es gibt Mockito-Matcher, die von Matchern im Hamcrest-Stil getrennt sind, sodass Beschreibungen übereinstimmender Ausdrücke direkt in Methodenaufrufe passen : Mockito-Matcher geben zurück, Two Hamcrest-Matcher-Methoden Matcher-Objekte (vom Typ Matcher<T>) zurückgeben.

Mockito Matcher werden durch statische Methoden aufgerufen wie eq, any, gt, und startsWithauf org.mockito.Matchersund org.mockito.AdditionalMatchers. Es gibt auch Adapter, die sich in den Mockito-Versionen geändert haben:

  • Bei Mockito 1.x handelt es sich Matchersbei einigen Aufrufen (z. B. intThatoder argThat) um Mockito-Matcher, die Hamcrest-Matcher direkt als Parameter akzeptieren. ArgumentMatcher<T>erweitert org.hamcrest.Matcher<T>, das in der internen Hamcrest-Darstellung verwendet wurde und eine Hamcrest-Matcher-Basisklasse anstelle eines Mockito-Matchers war.
  • Für Mockito 2.0+ ist Mockito nicht mehr direkt von Hamcrest abhängig. Matchersruft als intThatoder argThatumbrochene ArgumentMatcher<T>Objekte auf, die nicht mehr implementiert org.hamcrest.Matcher<T>sind, aber auf ähnliche Weise verwendet werden. Hamcrest-Adapter wie argThatund intThatsind noch verfügbar, wurden jedoch MockitoHamcreststattdessen geändert .

Unabhängig davon, ob die Matcher Hamcrest oder einfach Hamcrest-Stil sind, können sie wie folgt angepasst werden:

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

In der obigen Anweisung: foo.setPowerLevelist eine Methode, die eine akzeptiert int. is(greaterThan(9000))gibt a zurück Matcher<Integer>, was als setPowerLevelArgument nicht funktionieren würde . Der Mockito-Matcher intThatumschließt diesen Matcher im Hamcrest-Stil und gibt einen zurück, intdamit er als Argument angezeigt werden kann . Mockito-Matcher gt(9000)möchten den gesamten Ausdruck wie in der ersten Zeile des Beispielcodes in einen einzigen Aufruf einbinden.

Was Matcher tun / zurückgeben

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

Wenn Sie keine Argumentvergleicher verwenden, zeichnet Mockito Ihre Argumentwerte auf und vergleicht sie mit ihren equalsMethoden.

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

Wenn Sie einen Matcher wie anyoder gt(größer als) aufrufen , speichert Mockito ein Matcher-Objekt, das Mockito veranlasst, diese Gleichheitsprüfung zu überspringen und das Match Ihrer Wahl anzuwenden. Im Falle argumentCaptor.capture()speichert es einen Matcher, der sein Argument stattdessen für eine spätere Überprüfung speichert.

Matcher geben Dummy-Werte wie Null, leere Sammlungen oder zurück null. Mockito versucht, einen sicheren, geeigneten Dummy-Wert zurückzugeben, z. B. 0 für anyInt()oder any(Integer.class)oder leer List<String>für anyListOf(String.class). Aufgrund der Typlöschung fehlen Mockito jedoch Typinformationen , um einen beliebigen Wert außer nullfür any()oder zurückzugeben argThat(...), was zu einer NullPointerException führen kann, wenn versucht wird, einen nullprimitiven Wert automatisch zu entpacken .

Matcher mögen eqund gtnehmen Parameterwerte; Idealerweise sollten diese Werte berechnet werden, bevor das Stubbing / Verifizieren beginnt. Das Aufrufen eines Mocks während des Verspottens eines anderen Anrufs kann das Stubbing beeinträchtigen.

Matcher-Methoden können nicht als Rückgabewerte verwendet werden. Es gibt zum Beispiel keine Möglichkeit, thenReturn(anyInt())oder thenReturn(any(Foo.class))in Mockito zu formulieren. Mockito muss genau wissen, welche Instanz bei Stubbing-Aufrufen zurückgegeben werden soll, und wird keinen beliebigen Rückgabewert für Sie auswählen.

Implementierungsdetails

Matcher werden (als Objekt-Matcher im Hamcrest-Stil) in einem Stapel gespeichert, der in einer Klasse namens ArgumentMatcherStorage enthalten ist . MockitoCore und Matchers besitzen jeweils eine ThreadSafeMockingProgress- Instanz, die statisch eine ThreadLocal enthält, die MockingProgress-Instanzen enthält. Es ist dieses MockingProgressImpl , das ein konkretes ArgumentMatcherStorageImpl enthält . Folglich ist der Mock- und Matcher-Status statisch, aber zwischen den Klassen Mockito und Matchers konsistent.

Die meisten Matcher nur Anrufe zu diesem Stapel hinzufügen, mit einer Ausnahme für Matcher wie and, orundnot . Dies entspricht perfekt der Bewertungsreihenfolge von Java (und stützt sich darauf) , die Argumente von links nach rechts bewertet, bevor eine Methode aufgerufen wird:

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

Dieser Wille:

  1. In anyInt()auf den Stapel.
  2. In gt(10)auf den Stapel.
  3. In lt(20)auf den Stapel.
  4. Entfernen gt(10)und lt(20)hinzuzufügen and(gt(10), lt(20)).
  5. Aufruf foo.quux(0, 0), der (sofern nicht anders angegeben) den Standardwert zurückgibt false. Intern markiert Mockito quux(int, int)als den letzten Anruf.
  6. Call when(false), der sein Argument verwirft und sich auf quux(int, int)die in 5 identifizierte Stub-Methode vorbereitet . Die einzigen zwei gültigen Zustände haben die Stapellänge 0 (Gleichheit) oder 2 (Matcher), und es gibt zwei Matcher auf dem Stapel (Schritte 1 und 4) Mockito stoppt die Methode mit einem any()Matcher für das erste Argument und and(gt(10), lt(20))für das zweite Argument und löscht den Stapel.

Dies zeigt einige Regeln:

  • Mockito kann den Unterschied zwischen quux(anyInt(), 0)und nicht erkennen quux(0, anyInt()). Sie sehen beide aus wie ein Aufruf quux(0, 0)mit einem int Matcher auf dem Stapel. Wenn Sie also einen Matcher verwenden, müssen Sie alle Argumente abgleichen.

  • Anrufreihenfolge ist nicht nur wichtig, sondern macht das alles möglich . Das Extrahieren von Matchern zu Variablen funktioniert im Allgemeinen nicht, da normalerweise die Aufrufreihenfolge geändert wird. Das Extrahieren von Matchern zu Methoden funktioniert jedoch hervorragend.

    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.
    
  • Der Stapel wechselt oft genug, so dass Mockito ihn nicht sehr sorgfältig überwachen kann. Es kann den Stapel nur überprüfen, wenn Sie mit Mockito oder einem Mock interagieren, und muss Matcher akzeptieren, ohne zu wissen, ob sie sofort verwendet oder versehentlich aufgegeben werden. Theoretisch sollte der Stapel außerhalb eines Aufrufs von whenoder immer leer sein verify, aber Mockito kann dies nicht automatisch überprüfen. Sie können manuell überprüfen mit Mockito.validateMockitoUsage().

  • Bei einem Aufruf von whenruft Mockito tatsächlich die betreffende Methode auf, die eine Ausnahme auslöst, wenn Sie die Methode zum Auslösen einer Ausnahme gestoppt haben (oder Werte ungleich Null oder ungleich Null benötigen). doReturnund doAnswer(etc) rufen nicht die eigentliche Methode auf und sind oft eine nützliche Alternative.

  • Wenn Sie mitten im Stubbing eine Scheinmethode aufgerufen hätten (z. B. um eine Antwort für einen eqMatcher zu berechnen ), würde Mockito stattdessen die Stapellänge anhand dieses Aufrufs überprüfen und wahrscheinlich fehlschlagen.

  • Wenn Sie versuchen, etwas Schlechtes zu tun, z. B. eine endgültige Methode zu stubben / zu überprüfen , ruft Mockito die echte Methode auf und lässt zusätzliche Matcher auf dem Stapel . Der finalMethodenaufruf löst möglicherweise keine Ausnahme aus, aber Sie erhalten möglicherweise eine InvalidUseOfMatchersException von den streunenden Matchern, wenn Sie das nächste Mal mit einem Mock interagieren.

Allgemeine Probleme

  • InvalidUseOfMatchersException :

    • Stellen Sie sicher, dass jedes einzelne Argument genau einen Matcher-Aufruf enthält, wenn Sie überhaupt Matcher verwenden, und dass Sie keinen Matcher außerhalb eines whenoder verify-Aufrufs verwendet haben. Matcher sollten niemals als gestoppelte Rückgabewerte oder Felder / Variablen verwendet werden.

    • Stellen Sie sicher, dass Sie kein Mock aufrufen, um ein Matcher-Argument bereitzustellen.

    • Stellen Sie sicher, dass Sie nicht versuchen, eine endgültige Methode mit einem Matcher zu stub / verifizieren. Es ist eine großartige Möglichkeit, einen Matcher auf dem Stapel zu belassen. Wenn Ihre endgültige Methode keine Ausnahme auslöst, ist dies möglicherweise das einzige Mal, dass Sie feststellen, dass die Methode, über die Sie sich lustig machen, endgültig ist.

  • NullPointerException mit primitiven Argumenten: (Integer) any() gibt null zurück, während any(Integer.class)0 zurückgegeben wird; Dies kann dazu führen, NullPointerExceptiondass Sie eine intanstelle einer Ganzzahl erwarten . In jedem Fall bevorzugen anyInt(), die Null zurückgeben und auch den Auto-Boxing-Schritt überspringen.

  • Nullpointer oder andere Ausnahmen: Anrufe in denen when(foo.bar(any())).thenReturn(baz)tatsächlich nennen foo.bar(null) , die Sie stubbed haben könnten eine Ausnahme ausgelöst , wenn ein Null - Argument zu empfangen. Beim Umschalten auf doReturn(baz).when(foo).bar(any()) überspringt das Stubbed-Verhalten .

Allgemeine Fehlerbehebung

  • Verwenden Sie MockitoJUnitRunner oder rufen validateMockitoUsageSie explizit Ihre tearDownoder @After-Methode auf (was der Läufer automatisch für Sie tun würde). Auf diese Weise können Sie feststellen, ob Sie Matcher missbraucht haben.

  • Fügen Sie zum Debuggen Aufrufe validateMockitoUsagedirekt zu Ihrem Code hinzu. Dies wird ausgelöst, wenn Sie etwas auf dem Stapel haben, was eine gute Warnung vor einem schlechten Symptom ist.

Jeff Bowman
quelle
2
Danke für diesen Bericht. Eine NullPointerException mit dem Format when / thenReturn verursachte mir Probleme, bis ich sie in doReturn / when änderte.
Yngwietiger
11

Nur eine kleine Ergänzung zu Jeff Bowmans hervorragender Antwort, da ich diese Frage bei der Suche nach einer Lösung für eines meiner eigenen Probleme gefunden habe:

Wenn ein Aufruf einer Methode mit mehr als den whentrainierten Aufrufen eines Mocks übereinstimmt , ist die Reihenfolge der whenAufrufe wichtig und sollte von der breiteren bis zur spezifischsten sein. Ausgehend von einem von Jeffs Beispielen:

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

ist die Reihenfolge, die das (wahrscheinlich) gewünschte Ergebnis gewährleistet:

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

Wenn Sie die Wann-Aufrufe umkehren, ist das Ergebnis immer true.

tibtof
quelle
2
Obwohl dies nützliche Informationen sind, handelt es sich um Stubbing und nicht um Matcher. Daher ist dies in dieser Frage möglicherweise nicht sinnvoll. Die Reihenfolge spielt eine Rolle, aber nur insofern, als die zuletzt definierte Übereinstimmungskette gewinnt : Dies bedeutet, dass koexistierende Stubs häufig als am wenigsten spezifisch deklariert werden. In einigen Fällen möchten Sie jedoch möglicherweise eine sehr breite Überschreibung des spezifisch verspotteten Verhaltens in einem einzelnen Testfall An diesem Punkt muss möglicherweise eine umfassende Definition als letztes erfolgen.
Jeff Bowman
1
@ JeffBowman Ich dachte, dass es bei dieser Frage Sinn macht, da es sich um Mockito-Matcher handelt und Matcher beim Stubben verwendet werden können (wie in den meisten Ihrer Beispiele). Da ich bei Google nach einer Erklärung gesucht habe, halte ich es für nützlich, diese Informationen hier zu haben.
Tibtof