Ein Leitfaden für Unit-Testing-Praktiker zum alltäglichen Mockito

Veröffentlicht: 2022-03-11

Unit-Tests sind im Zeitalter von Agile obligatorisch geworden, und es stehen viele Tools zur Verfügung, die beim automatisierten Testen helfen. Ein solches Tool ist Mockito, ein Open-Source-Framework, mit dem Sie simulierte Objekte für Tests erstellen und konfigurieren können.

In diesem Artikel behandeln wir das Erstellen und Konfigurieren von Mocks und deren Verwendung, um das erwartete Verhalten des getesteten Systems zu überprüfen. Wir werden auch ein wenig in die Interna von Mockito eintauchen, um sein Design und seine Vorbehalte besser zu verstehen. Wir verwenden JUnit als Unit-Testing-Framework, aber da Mockito nicht an JUnit gebunden ist, können Sie mitmachen, auch wenn Sie ein anderes Framework verwenden.

Mockito erhalten

Mockito zu bekommen ist heutzutage einfach. Wenn Sie Gradle verwenden, müssen Sie Ihrem Build-Skript diese einzelne Zeile hinzufügen:

 testCompile "org.mockito:mockito−core:2.7.7"

Für diejenigen wie mich, die Maven immer noch bevorzugen, fügen Sie Mockito einfach wie folgt zu Ihren Abhängigkeiten hinzu:

 <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>2.7.7</version> <scope>test</scope> </dependency>

Natürlich ist die Welt viel größer als Maven und Gradle. Sie können jedes Projektmanagement-Tool verwenden, um das Mockito-Jar-Artefakt aus dem zentralen Maven-Repository abzurufen.

Annäherung an Mockito

Komponententests wurden entwickelt, um das Verhalten bestimmter Klassen oder Methoden zu testen, ohne sich auf das Verhalten ihrer Abhängigkeiten zu verlassen. Da wir die kleinste „Einheit“ des Codes testen, müssen wir keine tatsächlichen Implementierungen dieser Abhängigkeiten verwenden. Darüber hinaus verwenden wir leicht unterschiedliche Implementierungen dieser Abhängigkeiten, wenn wir unterschiedliche Verhaltensweisen testen. Ein traditioneller, bekannter Ansatz dafür ist das Erstellen von „Stubs“ – spezifische Implementierungen einer Schnittstelle, die für ein bestimmtes Szenario geeignet sind. Solche Implementierungen haben normalerweise fest codierte Logik. Ein Stub ist eine Art Testdouble. Andere Arten umfassen Fälschungen, Mocks, Spione, Dummies usw.

Wir konzentrieren uns nur auf zwei Arten von Testdoubles, „Mocks“ und „Spies“, da diese stark von Mockito eingesetzt werden.

Verspottet

Was ist spotten? Offensichtlich macht man sich hier nicht über seine Mitentwickler lustig. Mocking für Komponententests ist, wenn Sie ein Objekt erstellen, das das Verhalten eines echten Subsystems auf kontrollierte Weise implementiert. Kurz gesagt, Mocks werden als Ersatz für eine Abhängigkeit verwendet.

Mit Mockito erstellen Sie ein Mock, sagen Mockito, was zu tun ist, wenn bestimmte Methoden darauf aufgerufen werden, und verwenden dann die Mock-Instanz in Ihrem Test anstelle der echten. Nach dem Test können Sie den Mock abfragen, um zu sehen, welche spezifischen Methoden aufgerufen wurden, oder die Nebenwirkungen in Form des geänderten Zustands überprüfen.

Standardmäßig stellt Mockito eine Implementierung für jede Methode des Mock bereit.

Spione

Ein Spion ist die andere Art von Testdouble, die Mockito erstellt. Im Gegensatz zu Mocks erfordert das Erstellen eines Spions eine Instanz zum Ausspionieren. Standardmäßig delegiert ein Spion alle Methodenaufrufe an das reale Objekt und zeichnet auf, welche Methode aufgerufen wurde und mit welchen Parametern. Das macht es zu einem Spion: Es spioniert ein reales Objekt aus.

Erwägen Sie, wann immer möglich, Spott anstelle von Spionen zu verwenden. Spies können nützlich sein, um Legacy-Code zu testen, der nicht so umgestaltet werden kann, dass er leicht testbar ist, aber die Notwendigkeit, einen Spion einzusetzen, um eine Klasse teilweise zu verspotten, ist ein Indikator dafür, dass eine Klasse zu viel tut und somit gegen das Prinzip der Einzelverantwortung verstößt.

Erstellen eines einfachen Beispiels

Schauen wir uns eine einfache Demo an, für die wir Tests schreiben können. Angenommen, wir haben eine UserRepository Schnittstelle mit einer einzigen Methode, um einen Benutzer anhand seiner Kennung zu finden. Wir haben auch das Konzept eines Passwort-Encoders, um ein Klartext-Passwort in einen Passwort-Hash umzuwandeln. Sowohl UserRepository als auch PasswordEncoder sind Abhängigkeiten (auch Collaborators genannt) von UserService die über den Konstruktor eingefügt werden. So sieht unser Democode aus:

UserRepository
 public interface UserRepository { User findById(String id); }
Benutzer
 public class User { private String id; private String passwordHash; private boolean enabled; public User(String id, String passwordHash, boolean enabled) { this.id = id; this.passwordHash = passwordHash; this.enabled = enabled; } ... }
PasswortEncoder
 public interface PasswordEncoder { String encode(String password); }
UserService
 public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } public boolean isValidUser(String id, String password) { User user = userRepository.findById(id); return isEnabledUser(user) && isValidPassword(user, password); } private boolean isEnabledUser(User user) { return user != null && user.isEnabled(); } private boolean isValidPassword(User user, String password) { String encodedPassword = passwordEncoder.encode(password); return encodedPassword.equals(user.getPasswordHash()); } }

Dieser Beispielcode ist auf GitHub zu finden, sodass Sie ihn neben diesem Artikel zur Überprüfung herunterladen können.

Anwendung von Mockito

Sehen wir uns anhand unseres Beispielcodes an, wie Mockito angewendet und einige Tests geschrieben werden.

Mocks erstellen

Mit Mockito ist das Erstellen eines Mocks so einfach wie das Aufrufen einer statischen Methode Mockito.mock() :

 import static org.mockito.Mockito.*; ... PasswordEncoder passwordEncoder = mock(PasswordEncoder.class);

Beachten Sie den statischen Import für Mockito. Für den Rest dieses Artikels betrachten wir diesen Import implizit als hinzugefügt.

Nach dem Import verspotten wir PasswordEncoder , eine Schnittstelle. Mockito verspottet nicht nur Schnittstellen, sondern auch abstrakte Klassen und konkrete nicht-finale Klassen. Standardmäßig kann Mockito keine endgültigen Klassen und endgültigen oder statischen Methoden nachahmen, aber wenn Sie es wirklich brauchen, bietet Mockito 2 das experimentelle MockMaker-Plugin.

Beachten Sie auch, dass die Methoden equals() und hashCode() nicht verspottet werden können.

Spione erschaffen

Um einen Spion zu erstellen, müssen Sie die statische Methode spy() von Mockito aufrufen und ihr eine Instanz zum Ausspionieren übergeben. Das Aufrufen von Methoden des zurückgegebenen Objekts ruft echte Methoden auf, es sei denn, diese Methoden sind gestubbt. Diese Aufrufe werden aufgezeichnet und die Tatsachen dieser Aufrufe können verifiziert werden (siehe weitere Beschreibung von verify() ). Machen wir einen Spion:

 DecimalFormat decimalFormat = spy(new DecimalFormat()); assertEquals("42", decimalFormat.format(42L));

Das Erstellen eines Spions unterscheidet sich nicht wesentlich vom Erstellen eines Scheins. Darüber hinaus sind alle Mockito-Methoden, die zum Konfigurieren eines Mocks verwendet werden, auch zum Konfigurieren eines Spions anwendbar.

Spies werden im Vergleich zu Mocks selten verwendet, aber Sie können sie nützlich finden, um Legacy-Code zu testen, der nicht umgestaltet werden kann, wenn das Testen teilweises Mocking erfordert. In diesen Fällen können Sie einfach einen Spion erstellen und einige seiner Methoden kürzen, um das gewünschte Verhalten zu erzielen.

Standardrückgabewerte

Der Aufruf von mock(PasswordEncoder.class) gibt eine Instanz von PasswordEncoder zurück. Wir können sogar seine Methoden aufrufen, aber was werden sie zurückgeben? Standardmäßig geben alle Methoden eines Mocks „nicht initialisierte“ oder „leere“ Werte zurück, z. B. Nullen für numerische Typen (sowohl primitive als auch geschachtelte), false für boolesche Werte und Nullen für die meisten anderen Typen.

Betrachten Sie die folgende Schnittstelle:

 interface Demo { int getInt(); Integer getInteger(); double getDouble(); boolean getBoolean(); String getObject(); Collection<String> getCollection(); String[] getArray(); Stream<?> getStream(); Optional<?> getOptional(); }

Betrachten Sie nun das folgende Snippet, das eine Vorstellung davon gibt, welche Standardwerte von den Methoden eines Mocks zu erwarten sind:

 Demo demo = mock(Demo.class); assertEquals(0, demo.getInt()); assertEquals(0, demo.getInteger().intValue()); assertEquals(0d, demo.getDouble(), 0d); assertFalse(demo.getBoolean()); assertNull(demo.getObject()); assertEquals(Collections.emptyList(), demo.getCollection()); assertNull(demo.getArray()); assertEquals(0L, demo.getStream().count()); assertFalse(demo.getOptional().isPresent());

Stubbing-Methoden

Frische, unveränderte Mocks sind nur in seltenen Fällen sinnvoll. Normalerweise möchten wir den Mock konfigurieren und definieren, was zu tun ist, wenn bestimmte Methoden des Mocks aufgerufen werden. Dies wird Stubben genannt.

Mockito bietet zwei Möglichkeiten zum Stubben. Der erste Weg ist „ wenn diese Methode aufgerufen wird, dann tue etwas.“ Betrachten Sie den folgenden Ausschnitt:

 when(passwordEncoder.encode("1")).thenReturn("a");

Es liest sich fast wie Englisch: „Wenn passwordEncoder.encode(“1”) aufgerufen wird, gib ein a zurück.“

Die zweite Art des Stubbings lautet eher wie „Tue etwas, wenn die Methode dieses Mocks mit den folgenden Argumenten aufgerufen wird.“ Diese Art des Stubbens ist schwieriger zu lesen, da die Ursache am Ende angegeben wird. Erwägen:

 doReturn("a").when(passwordEncoder).encode("1");

Das Snippet mit dieser Stubbing-Methode würde lauten: „Gib a zurück, wenn die Methode encode() von passwordEncoder mit einem Argument von 1 aufgerufen wird.“

Der erste Weg wird als bevorzugt angesehen, da er typsicher und besser lesbar ist. In seltenen Fällen sind Sie jedoch gezwungen, den zweiten Weg zu verwenden, z. B. wenn Sie eine echte Methode eines Spions abstumpfen, da das Aufrufen dieser unerwünschte Nebenwirkungen haben kann.

Lassen Sie uns kurz die von Mockito bereitgestellten Stubbing-Methoden untersuchen. Wir werden beide Stubbing-Methoden in unsere Beispiele einbeziehen.

Zurückgeben von Werten

thenReturn oder doReturn() werden verwendet, um einen Wert anzugeben, der beim Methodenaufruf zurückgegeben werden soll.

 //”when this method is called, then do something” when(passwordEncoder.encode("1")).thenReturn("a");

oder

 //”do something when this mock's method is called with the following arguments” doReturn("a").when(passwordEncoder).encode("1");

Sie können auch mehrere Werte angeben, die als Ergebnisse aufeinanderfolgender Methodenaufrufe zurückgegeben werden. Der letzte Wert wird als Ergebnis für alle weiteren Methodenaufrufe verwendet.

 //when when(passwordEncoder.encode("1")).thenReturn("a", "b");

oder

 //do doReturn("a", "b").when(passwordEncoder).encode("1");

Dasselbe kann mit dem folgenden Snippet erreicht werden:

 when(passwordEncoder.encode("1")) .thenReturn("a") .thenReturn("b");

Dieses Muster kann auch mit anderen Stubbing-Methoden verwendet werden, um die Ergebnisse aufeinanderfolgender Aufrufe zu definieren.

Benutzerdefinierte Antworten zurückgeben

then() , ein Alias ​​für thenAnswer() , und doAnswer() erreichen dasselbe, indem eine benutzerdefinierte Antwort eingerichtet wird, die zurückgegeben wird, wenn eine Methode aufgerufen wird, etwa so:

 when(passwordEncoder.encode("1")).thenAnswer( invocation -> invocation.getArgument(0) + "!");

oder

 doAnswer(invocation -> invocation.getArgument(0) + "!") .when(passwordEncoder).encode("1");

Das einzige Argument, thenAnswer() , ist eine Implementierung der Answer -Schnittstelle. Es hat eine einzelne Methode mit einem Parameter vom Typ InvocationOnMock .

Sie können auch eine Ausnahme als Ergebnis eines Methodenaufrufs auslösen:

 when(passwordEncoder.encode("1")).thenAnswer(invocation -> { throw new IllegalArgumentException(); });

…oder die eigentliche Methode einer Klasse aufrufen (gilt nicht für Interfaces):

 Date mock = mock(Date.class); doAnswer(InvocationOnMock::callRealMethod).when(mock).setTime(42); doAnswer(InvocationOnMock::callRealMethod).when(mock).getTime(); mock.setTime(42); assertEquals(42, mock.getTime());

Du hast Recht, wenn du denkst, dass es umständlich aussieht. Mockito stellt thenCallRealMethod() und thenThrow() , um diesen Aspekt Ihres Testens zu optimieren.

Aufrufen von echten Methoden

Wie der Name schon sagt, thenCallRealMethod() und doCallRealMethod() die reale Methode für ein Scheinobjekt auf:

 Date mock = mock(Date.class); when(mock.getTime()).thenCallRealMethod(); doCallRealMethod().when(mock).setTime(42); mock.setTime(42); assertEquals(42, mock.getTime());

Das Aufrufen echter Methoden kann bei partiellen Mocks nützlich sein, aber stellen Sie sicher, dass die aufgerufene Methode keine unerwünschten Nebenwirkungen hat und nicht vom Objektstatus abhängt. Wenn dies der Fall ist, ist ein Spion möglicherweise besser geeignet als ein Schein.

Wenn Sie ein Mock einer Schnittstelle erstellen und versuchen, einen Stub so zu konfigurieren, dass er eine echte Methode aufruft, löst Mockito eine Ausnahme mit einer sehr informativen Nachricht aus. Betrachten Sie den folgenden Ausschnitt:

 when(passwordEncoder.encode("1")).thenCallRealMethod();

Mockito schlägt mit der folgenden Meldung fehl:

 Cannot call abstract real method on java object! Calling real methods is only possible when mocking non abstract method. //correct example: when(mockOfConcreteClass.nonAbstractMethod()).thenCallRealMethod();

Ein großes Lob an die Mockito-Entwickler, die sich so um sie gekümmert haben, um so gründliche Beschreibungen bereitzustellen!

Ausnahmen werfen

thenThrow() und doThrow() konfigurieren eine verspottete Methode, um eine Ausnahme auszulösen:

 when(passwordEncoder.encode("1")).thenThrow(new IllegalArgumentException());

oder

 doThrow(new IllegalArgumentException()).when(passwordEncoder).encode("1");

Mockito stellt sicher, dass die ausgelöste Ausnahme für diese bestimmte Stub-Methode gültig ist, und beschwert sich, wenn die Ausnahme nicht in der Liste der geprüften Ausnahmen der Methode enthalten ist. Folgendes berücksichtigen:

 when(passwordEncoder.encode("1")).thenThrow(new IOException());

Es wird zu einem Fehler führen:

 org.mockito.exceptions.base.MockitoException: Checked exception is invalid for this method! Invalid: java.io.IOException

Wie Sie sehen können, hat Mockito festgestellt, dass encode() keine IOException kann.

Sie können auch die Klasse einer Ausnahme übergeben, anstatt eine Instanz einer Ausnahme zu übergeben:

 when(passwordEncoder.encode("1")).thenThrow(IllegalArgumentException.class);

oder

 doThrow(IllegalArgumentException.class).when(passwordEncoder).encode("1");

Allerdings kann Mockito eine Ausnahmeklasse nicht auf die gleiche Weise validieren wie eine Ausnahmeinstanz, also müssen Sie diszipliniert sein und dürfen keine illegalen Klassenobjekte übergeben. Folgendes löst beispielsweise IOException , obwohl von encode() nicht erwartet wird, dass es eine geprüfte Ausnahme auslöst:

 when(passwordEncoder.encode("1")).thenThrow(IOException.class); passwordEncoder.encode("1");

Verspotten von Schnittstellen mit Standardmethoden

Es ist erwähnenswert, dass Mockito beim Erstellen eines Mocks für eine Schnittstelle alle Methoden dieser Schnittstelle mockt. Seit Java 8 können Schnittstellen neben abstrakten auch Standardmethoden enthalten. Diese Methoden werden ebenfalls verspottet, daher müssen Sie darauf achten, dass sie als Standardmethoden fungieren.

Betrachten Sie das folgende Beispiel:

 interface AnInterface { default boolean isTrue() { return true; } } AnInterface mock = mock(AnInterface.class); assertFalse(mock.isTrue());

In diesem Beispiel ist assertFalse() erfolgreich. Wenn Sie das nicht erwartet haben, stellen Sie sicher, dass Mockito die echte Methode aufgerufen hat, etwa so:

 AnInterface mock = mock(AnInterface.class); when(mock.isTrue()).thenCallRealMethod(); assertTrue(mock.isTrue());

Argument-Matcher

In den vorherigen Abschnitten haben wir unsere simulierten Methoden mit exakten Werten als Argumente konfiguriert. In diesen Fällen ruft Mockito einfach intern equals() auf, um zu prüfen, ob die erwarteten Werte gleich den tatsächlichen Werten sind.

Manchmal kennen wir diese Werte jedoch vorher nicht.

Vielleicht ist es uns einfach egal, ob der tatsächliche Wert als Argument übergeben wird, oder vielleicht möchten wir eine Reaktion für einen größeren Bereich von Werten definieren. All diese Szenarien (und mehr) können mit Argument-Matchern angegangen werden. Die Idee ist einfach: Anstatt einen genauen Wert bereitzustellen, stellen Sie einen Argument-Matcher für Mockito bereit, mit dem Methodenargumente abgeglichen werden können.

Betrachten Sie den folgenden Ausschnitt:

 when(passwordEncoder.encode(anyString())).thenReturn("exact"); assertEquals("exact", passwordEncoder.encode("1")); assertEquals("exact", passwordEncoder.encode("abc"));

Sie können sehen, dass das Ergebnis dasselbe ist, egal welchen Wert wir an encode() übergeben, weil wir in dieser ersten Zeile den Argument-Matcher anyString() verwendet haben. Wenn wir diese Zeile in Klartext umschreiben, würde es so klingen: „Wenn der Passwort-Encoder aufgefordert wird, eine Zeichenfolge zu codieren, geben Sie die Zeichenfolge „exakt“ zurück.“

Mockito verlangt, dass Sie alle Argumente entweder nach Matchern oder nach exakten Werten angeben. Wenn also eine Methode mehr als ein Argument hat und Sie Argument-Matcher nur für einige ihrer Argumente verwenden möchten, vergessen Sie es. Sie können keinen Code wie diesen schreiben:

 abstract class AClass { public abstract boolean call(String s, int i); } AClass mock = mock(AClass.class); //This doesn't work. when(mock.call("a", anyInt())).thenReturn(true);

Um den Fehler zu beheben, müssen wir die letzte Zeile ersetzen, um den eq -Argument-Matcher für a wie folgt einzuschließen:

 when(mock.call(eq("a"), anyInt())).thenReturn(true);

Hier haben wir die Argument-Matcher eq() und anyInt() verwendet, aber es sind noch viele andere verfügbar. Eine vollständige Liste der Argument-Matcher finden Sie in der Dokumentation zur Klasse org.mockito.ArgumentMatchers .

Es ist wichtig zu beachten, dass Sie keine Argument-Matcher außerhalb der Verifizierung oder Stubbing verwenden können. Sie können beispielsweise Folgendes nicht haben:

 //this won't work String orMatcher = or(eq("a"), endsWith("b")); verify(mock).encode(orMatcher);

Mockito erkennt den falsch platzierten Argument-Matcher und löst eine InvalidUseOfMatchersException . Die Überprüfung mit Argument-Matchern sollte folgendermaßen erfolgen:

 verify(mock).encode(or(eq("a"), endsWith("b")));

Argument-Matcher können auch nicht als Rückgabewert verwendet werden. Mockito kann nicht anyString() oder was auch immer zurückgeben; Beim Stubben von Anrufen ist ein exakter Wert erforderlich.

Benutzerdefinierte Matcher

Benutzerdefinierte Matcher kommen zu Hilfe, wenn Sie eine Matching-Logik bereitstellen müssen, die in Mockito noch nicht verfügbar ist. Die Entscheidung, einen benutzerdefinierten Matcher zu erstellen, sollte nicht auf die leichte Schulter genommen werden, da die Notwendigkeit, Argumente auf nicht triviale Weise abzugleichen, entweder auf ein Problem im Design oder darauf hinweist, dass ein Test zu kompliziert wird.

Daher lohnt es sich zu prüfen, ob Sie einen Test vereinfachen können, indem Sie einige der nachsichtigen Argument-Matcher wie isNull() und nullable() verwenden, bevor Sie einen benutzerdefinierten Matcher schreiben. Wenn Sie immer noch das Bedürfnis haben, einen Argument-Matcher zu schreiben, bietet Mockito eine Familie von Methoden, um dies zu tun.

Betrachten Sie das folgende Beispiel:

 FileFilter fileFilter = mock(FileFilter.class); ArgumentMatcher<File> hasLuck = file -> file.getName().endsWith("luck"); when(fileFilter.accept(argThat(hasLuck))).thenReturn(true); assertFalse(fileFilter.accept(new File("/deserve"))); assertTrue(fileFilter.accept(new File("/deserve/luck")));

Hier erstellen wir den hasLuck Argument-Matcher und verwenden argThat() , um den Matcher als Argument an eine verspottete Methode zu übergeben und ihn so zu stubbeln, dass er true zurückgibt, wenn der Dateiname mit „Glück“ endet. Sie können ArgumentMatcher als funktionale Schnittstelle behandeln und seine Instanz mit einem Lambda erstellen (was wir im Beispiel getan haben). Eine weniger prägnante Syntax würde wie folgt aussehen:

 ArgumentMatcher<File> hasLuck = new ArgumentMatcher<File>() { @Override public boolean matches(File file) { return file.getName().endsWith("luck"); } };

Wenn Sie einen Argument-Matcher erstellen müssen, der mit primitiven Typen funktioniert, gibt es mehrere andere Methoden dafür in org.mockito.ArgumentMatchers :

  • charThat(ArgumentMatcher<Zeichen>Matcher)
  • booleanThat(ArgumentMatcher<Boolean>Matcher)
  • byteThat(ArgumentMatcher<Byte>-Matcher)
  • shortThat(ArgumentMatcher<Short>-Matcher)
  • intThat(ArgumentMatcher<Integer>-Matcher)
  • longThat(ArgumentMatcher<Long>-Matcher)
  • floatThat(ArgumentMatcher<Float>-Matcher)
  • doubleThat(ArgumentMatcher<Double>Matcher)

Matcher kombinieren

Es lohnt sich nicht immer, einen benutzerdefinierten Argument-Matcher zu erstellen, wenn eine Bedingung zu kompliziert ist, um mit einfachen Matchern behandelt zu werden. Manchmal reicht die Kombination von Matchern aus. Mockito bietet Argument-Matcher, um gängige logische Operationen ('not', 'and', 'or') für Argument-Matcher zu implementieren, die sowohl mit primitiven als auch mit nicht-primitiven Typen übereinstimmen. Diese Matcher sind als statische Methoden in der Klasse org.mockito.AdditionalMatchers .

Betrachten Sie das folgende Beispiel:

 when(passwordEncoder.encode(or(eq("1"), contains("a")))).thenReturn("ok"); assertEquals("ok", passwordEncoder.encode("1")); assertEquals("ok", passwordEncoder.encode("123abc")); assertNull(passwordEncoder.encode("123"));

Hier haben wir die Ergebnisse von zwei Argument-Matchern kombiniert: eq("1") und contains("a") . Der abschließende Ausdruck or(eq("1"), contains("a")) kann interpretiert werden als „die Argumentzeichenfolge muss gleich „1“ sein oder „a“ enthalten “.

Beachten Sie, dass in der Klasse org.mockito.AdditionalMatchers weniger gebräuchliche Matcher aufgeführt sind, z. B. geq() , leq() , gt() und lt() , bei denen es sich um Wertvergleiche handelt, die für primitive Werte und Instanzen von java.lang.Comparable .

Verhalten überprüfen

Sobald ein Mock oder Spion verwendet wurde, können wir verify , ob bestimmte Interaktionen stattgefunden haben. Wörtlich sagen wir: „Hey, Mockito, stellen Sie sicher, dass diese Methode mit diesen Argumenten aufgerufen wurde.“

Betrachten Sie das folgende künstliche Beispiel:

 PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); when(passwordEncoder.encode("a")).thenReturn("1"); passwordEncoder.encode("a"); verify(passwordEncoder).encode("a");

Hier haben wir einen Mock eingerichtet und seine Methode encode() aufgerufen. Die letzte Zeile überprüft, ob die Methode encode() des Modells mit dem spezifischen Argumentwert a aufgerufen wurde. Bitte beachten Sie, dass die Überprüfung eines Stub-Aufrufs überflüssig ist; Der Zweck des vorherigen Snippets besteht darin, die Idee zu zeigen, eine Überprüfung durchzuführen, nachdem einige Interaktionen stattgefunden haben.

Wenn wir die letzte Zeile so ändern, dass sie ein anderes Argument hat – sagen wir b –, schlägt der vorherige Test fehl und Mockito beschwert sich, dass der tatsächliche Aufruf andere Argumente hat ( b anstelle des erwarteten a ).

Argument-Matcher können zur Verifizierung genauso wie zum Stubbing verwendet werden:

 verify(passwordEncoder).encode(anyString());

Standardmäßig überprüft Mockito, ob die Methode einmal aufgerufen wurde, aber Sie können eine beliebige Anzahl von Aufrufen überprüfen:

 // verify the exact number of invocations verify(passwordEncoder, times(42)).encode(anyString()); // verify that there was at least one invocation verify(passwordEncoder, atLeastOnce()).encode(anyString()); // verify that there were at least five invocations verify(passwordEncoder, atLeast(5)).encode(anyString()); // verify the maximum number of invocations verify(passwordEncoder, atMost(5)).encode(anyString()); // verify that it was the only invocation and // that there're no more unverified interactions verify(passwordEncoder, only()).encode(anyString()); // verify that there were no invocations verify(passwordEncoder, never()).encode(anyString());

Eine selten genutzte Funktion von verify() ist die Fähigkeit, bei einem Timeout fehlzuschlagen, was hauptsächlich zum Testen von nebenläufigem Code nützlich ist. Wenn beispielsweise unser Passwort-Encoder in einem anderen Thread gleichzeitig mit verify() aufgerufen wird, können wir einen Test wie folgt schreiben:

 usePasswordEncoderInOtherThread(); verify(passwordEncoder, timeout(500)).encode("a");

Dieser Test ist erfolgreich, wenn encode() innerhalb von 500 Millisekunden oder weniger aufgerufen und beendet wird. Wenn Sie den gesamten von Ihnen angegebenen Zeitraum warten müssen, verwenden Sie after() anstelle von timeout() :

 verify(passwordEncoder, after(500)).encode("a");

Andere Überprüfungsmodi ( times() , atLeast() usw.) können mit timeout() und after() kombiniert werden, um kompliziertere Tests durchzuführen:

 // passes as soon as encode() has been called 3 times within 500 ms verify(passwordEncoder, timeout(500).times(3)).encode("a");

Neben times() gehören zu den unterstützten Überprüfungsmodi only() , atLeast() und atLeastOnce() (als Alias ​​für atLeast(1) ).

Mit Mockito können Sie auch die Anrufreihenfolge in einer Gruppe von Mocks überprüfen. Es ist keine Funktion, die sehr oft verwendet wird, aber es kann nützlich sein, wenn die Reihenfolge der Aufrufe wichtig ist. Betrachten Sie das folgende Beispiel:

 PasswordEncoder first = mock(PasswordEncoder.class); PasswordEncoder second = mock(PasswordEncoder.class); // simulate calls first.encode("f1"); second.encode("s1"); first.encode("f2"); // verify call order InOrder inOrder = inOrder(first, second); inOrder.verify(first).encode("f1"); inOrder.verify(second).encode("s1"); inOrder.verify(first).encode("f2");

Wenn wir die Reihenfolge der simulierten Aufrufe ändern, schlägt der Test mit VerificationInOrderFailure fehl.

Das Fehlen von Aufrufen kann auch mit verifyZeroInteractions() verifiziert werden. Diese Methode akzeptiert einen oder mehrere Mocks als Argument und schlägt fehl, wenn Methoden der übergebenen Mocks aufgerufen wurden.

Es ist auch erwähnenswert, die Methode verifyNoMoreInteractions() zu erwähnen, da sie Mocks als Argument akzeptiert und verwendet werden kann, um zu überprüfen, ob jeder Aufruf dieser Mocks verifiziert wurde.

Argumente erfassen

Neben der Überprüfung, ob eine Methode mit bestimmten Argumenten aufgerufen wurde, ermöglicht Ihnen Mockito, diese Argumente zu erfassen, damit Sie später benutzerdefinierte Assertionen darauf ausführen können. Mit anderen Worten, Sie sagen: „Hey, Mockito, überprüfen Sie, ob diese Methode aufgerufen wurde, und geben Sie mir die Argumentwerte, mit denen sie aufgerufen wurde.“

Lassen Sie uns ein Mock von PasswordEncoder erstellen, encode() aufrufen, das Argument erfassen und seinen Wert überprüfen:

 PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("password", passwordCaptor.getValue());

Wie Sie sehen können, übergeben wir zur Überprüfung passwordCaptor.capture() als Argument von encode() ; Dadurch wird intern ein Argument-Matcher erstellt, der das Argument speichert. Dann rufen wir den erfassten Wert mit passwordCaptor.getValue() ab und prüfen ihn mit assertEquals() .

Wenn wir ein Argument über mehrere Aufrufe hinweg erfassen müssen, können Sie mit ArgumentCaptor alle Werte mit getAllValues() , etwa so:

 PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password1"); passwordEncoder.encode("password2"); passwordEncoder.encode("password3"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder, times(3)).encode(passwordCaptor.capture()); assertEquals(Arrays.asList("password1", "password2", "password3"), passwordCaptor.getAllValues());

Dieselbe Technik kann zum Erfassen von Methodenargumenten mit variabler Stellenzahl (auch als Varargs bezeichnet) verwendet werden.

Testen unseres einfachen Beispiels

Jetzt, da wir viel mehr über Mockito wissen, ist es an der Zeit, zu unserer Demo zurückzukehren. Lassen Sie uns den isValidUser -Methodentest schreiben. So könnte es aussehen:

 public class UserServiceTest { private static final String PASSWORD = "password"; private static final User ENABLED_USER = new User("user id", "hash", true); private static final User DISABLED_USER = new User("disabled user id", "disabled user password hash", false); private UserRepository userRepository; private PasswordEncoder passwordEncoder; private UserService userService; @Before public void setup() { userRepository = createUserRepository(); passwordEncoder = createPasswordEncoder(); userService = new UserService(userRepository, passwordEncoder); } @Test public void shouldBeValidForValidCredentials() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), PASSWORD); assertTrue(userIsValid); // userRepository had to be used to find a user with verify(userRepository).findById(ENABLED_USER.getId()); // passwordEncoder had to be used to compute a hash of "password" verify(passwordEncoder).encode(PASSWORD); } @Test public void shouldBeInvalidForInvalidId() { boolean userIsValid = userService.isValidUser("invalid id", PASSWORD); assertFalse(userIsValid); InOrder inOrder = inOrder(userRepository, passwordEncoder); inOrder.verify(userRepository).findById("invalid id"); inOrder.verify(passwordEncoder, never()).encode(anyString()); } @Test public void shouldBeInvalidForInvalidPassword() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), "invalid"); assertFalse(userIsValid); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("invalid", passwordCaptor.getValue()); } @Test public void shouldBeInvalidForDisabledUser() { boolean userIsValid = userService.isValidUser(DISABLED_USER.getId(), PASSWORD); assertFalse(userIsValid); verify(userRepository).findById(DISABLED_USER.getId()); verifyZeroInteractions(passwordEncoder); } private PasswordEncoder createPasswordEncoder() { PasswordEncoder mock = mock(PasswordEncoder.class); when(mock.encode(anyString())).thenReturn("any password hash"); when(mock.encode(PASSWORD)).thenReturn(ENABLED_USER.getPasswordHash()); return mock; } private UserRepository createUserRepository() { UserRepository mock = mock(UserRepository.class); when(mock.findById(ENABLED_USER.getId())).thenReturn(ENABLED_USER); when(mock.findById(DISABLED_USER.getId())).thenReturn(DISABLED_USER); return mock; } }

Tauchen unter der API

Mockito bietet eine lesbare, praktische API, aber lassen Sie uns einige seiner internen Funktionsweisen untersuchen, um seine Einschränkungen zu verstehen und seltsame Fehler zu vermeiden.

Lassen Sie uns untersuchen, was in Mockito vor sich geht, wenn das folgende Snippet ausgeführt wird:

 // 1: create PasswordEncoder mock = mock(PasswordEncoder.class); // 2: stub when(mock.encode("a")).thenReturn("1"); // 3: act mock.encode("a"); // 4: verify verify(mock).encode(or(eq("a"), endsWith("b")));

Offensichtlich erzeugt die erste Zeile einen Mock. Mockito verwendet ByteBuddy, um eine Unterklasse der angegebenen Klasse zu erstellen. Das neue Klassenobjekt hat einen generierten Namen wie demo.mockito.PasswordEncoder$MockitoMock$1953422997 , seine equals() Funktion prüft die Identität und hashCode() gibt einen Identitäts-Hashcode zurück. Sobald die Klasse generiert und geladen ist, wird ihre Instanz mit Objenesis erstellt.

Schauen wir uns die nächste Zeile an:

 when(mock.encode("a")).thenReturn("1");

Die Reihenfolge ist wichtig: Die erste hier ausgeführte Anweisung ist mock.encode("a") , die encode() auf dem Mock mit einem Standardrückgabewert von null aufruft. Also wirklich, wir übergeben null als Argument von when() . Mockito kümmert sich nicht darum, welcher genaue Wert an when() übergeben wird, da es Informationen über den Aufruf einer verspotteten Methode im sogenannten „ongoing stubbing“ speichert, wenn diese Methode aufgerufen wird. Später, wenn wir when() aufrufen, zieht Mockito dieses fortlaufende Stubbing-Objekt und gibt es als Ergebnis von when() zurück. Dann rufen wir thenReturn(“1”) für das zurückgegebene fortlaufende Stubbing-Objekt auf.

Die dritte Zeile, mock.encode("a"); ist einfach: Wir rufen die Stubbed-Methode auf. Intern speichert Mockito diesen Aufruf zur weiteren Überprüfung und gibt die Stub-Antwort auf den Aufruf zurück; In unserem Fall ist es die Zeichenfolge 1 .

In der vierten Zeile ( verify(mock).encode(or(eq("a"), endsWith("b"))); ) bitten wir Mockito zu überprüfen, ob es einen Aufruf von encode() mit diesen gab konkrete Argumente.

verify() wird zuerst ausgeführt, wodurch Mockitos interner Zustand in den Verifizierungsmodus versetzt wird. Es ist wichtig zu verstehen, dass Mockito seinen Status in einem ThreadLocal . Dadurch lässt sich zwar eine schöne Syntax implementieren, andererseits kann es aber auch zu merkwürdigem Verhalten führen, wenn das Framework falsch verwendet wird (wenn Sie beispielsweise versuchen, Argument-Matcher außerhalb von Verifikation oder Stubbing zu verwenden).

Wie erstellt Mockito also einen or -Matcher? Zuerst wird eq("a") aufgerufen, und dem Matcher-Stack wird ein equals -Matcher hinzugefügt. Zweitens wird endsWith("b") aufgerufen, und dem Stack wird ein endsWith -Matcher hinzugefügt. Zuletzt wird or(null, null) aufgerufen – es verwendet die beiden Matcher, die es aus dem Stack holt, erstellt den or -Matcher und schiebt diesen auf den Stack. Schließlich wird encode() aufgerufen. Mockito überprüft dann, ob die Methode so oft wie erwartet und mit den erwarteten Argumenten aufgerufen wurde.

Während Argument-Matcher nicht in Variablen extrahiert werden können (weil sie die Aufrufreihenfolge ändern), können sie in Methoden extrahiert werden. Dies bewahrt die Aufrufreihenfolge und hält den Stack im richtigen Zustand:

 verify(mock).encode(matchCondition()); … String matchCondition() { return or(eq("a"), endsWith("b")); }

Standardantworten ändern

In den vorherigen Abschnitten haben wir unsere Mocks so erstellt, dass sie beim Aufrufen von mockierten Methoden einen „leeren“ Wert zurückgeben. Dieses Verhalten ist konfigurierbar. Sie können sogar Ihre eigene Implementierung von org.mockito.stubbing.Answer , wenn die von Mockito bereitgestellten nicht geeignet sind, aber es könnte ein Hinweis darauf sein, dass etwas nicht stimmt, wenn Unit-Tests zu kompliziert werden. Denken Sie an das KISS-Prinzip!

Sehen wir uns Mockitos Angebot an vordefinierten Standardantworten an:

  • RETURNS_DEFAULTS ist die Standardstrategie; Es ist nicht erwähnenswert, wenn Sie einen Mock einrichten.

  • CALLS_REAL_METHODS , dass ungestubbte Aufrufe echte Methoden aufrufen.

  • RETURNS_SMART_NULLS vermeidet eine NullPointerException , indem SmartNull anstelle von null zurückgegeben wird, wenn ein Objekt verwendet wird, das von einem unstubbed-Methodenaufruf zurückgegeben wird. Sie werden immer noch mit einer NullPointerException , aber SmartNull gibt Ihnen einen schöneren Stack-Trace mit der Zeile, in der die unstubbed-Methode aufgerufen wurde. Daher lohnt es sich, RETURNS_SMART_NULLS als Standardantwort in Mockito zu haben!

  • RETURNS_MOCKS versucht zuerst, gewöhnliche „leere“ Werte zurückzugeben, verspottet dann, wenn möglich, und ist ansonsten null . Das Kriterium der Leerheit unterscheidet sich ein wenig von dem, was wir zuvor gesehen haben: Anstatt null für Strings und Arrays zurückzugeben, geben die mit RETURNS_MOCKS erstellten Mocks leere Strings bzw. leere Arrays zurück.

  • RETURNS_SELF ist nützlich, um Bauherren zu verspotten. Mit dieser Einstellung gibt ein Mock eine Instanz von sich selbst zurück, wenn eine Methode aufgerufen wird, die einen Typ zurückgibt, der der Klasse (oder einer Oberklasse) der gemockten Klasse entspricht.

  • RETURNS_DEEP_STUBS geht tiefer als RETURNS_MOCKS und erstellt Mocks, die in der Lage sind, Mocks von Mocks von Mocks usw. zurückzugeben. Im Gegensatz zu RETURNS_MOCKS sind die Leerheitsregeln in RETURNS_DEEP_STUBS standardmäßig vorhanden, sodass für Strings und Arrays null zurückgegeben wird:

 interface We { Are we(); } interface Are { So are(); } interface So { Deep so(); } interface Deep { boolean deep(); } ... We mock = mock(We.class, Mockito.RETURNS_DEEP_STUBS); when(mock.we().are().so().deep()).thenReturn(true); assertTrue(mock.we().are().so().deep());

Einen Mock benennen

Mit Mockito können Sie einen Mock benennen, eine Funktion, die nützlich ist, wenn Sie viele Mocks in einem Test haben und sie unterscheiden müssen. Allerdings könnte die Notwendigkeit, Mocks zu benennen, ein Symptom für schlechtes Design sein. Folgendes berücksichtigen:

 PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class); verify(robustPasswordEncoder).encode(anyString());

Mockito wird sich beschweren, aber da wir die Mocks nicht offiziell benannt haben, wissen wir nicht, welcher:

 Wanted but not invoked: passwordEncoder.encode(<any string>);

Benennen wir sie, indem wir eine Zeichenfolge für die Konstruktion übergeben:

 PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class, "robustPasswordEncoder"); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class, "weakPasswordEncoder"); verify(robustPasswordEncoder).encode(anyString());

Jetzt ist die Fehlermeldung freundlicher und weist eindeutig auf robustPasswordEncoder :

 Wanted but not invoked: robustPasswordEncoder.encode(<any string>);

Implementieren mehrerer Mock-Schnittstellen

Sometimes, you may wish to create a mock that implements several interfaces. Mockito is able to do that easily, like so:

 PasswordEncoder mock = mock( PasswordEncoder.class, withSettings().extraInterfaces(List.class, Map.class)); assertTrue(mock instanceof List); assertTrue(mock instanceof Map);

Listening Invocations

A mock can be configured to call an invocation listener every time a method of the mock was called. Inside the listener, you can find out whether the invocation produced a value or if an exception was thrown.

 InvocationListener invocationListener = new InvocationListener() { @Override public void reportInvocation(MethodInvocationReport report) { if (report.threwException()) { Throwable throwable = report.getThrowable(); // do something with throwable throwable.printStackTrace(); } else { Object returnedValue = report.getReturnedValue(); // do something with returnedValue System.out.println(returnedValue); } } }; PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().invocationListeners(invocationListener)); passwordEncoder.encode("1");

In this example, we're dumping either the returned value or a stack trace to a system output stream. Our implementation does roughly the same as Mockito's org.mockito.internal.debugging.VerboseMockInvocationLogger (don't use this directly, it's internal stuff). If logging invocations is the only feature you need from the listener, then Mockito provides a cleaner way to express your intent with the verboseLogging() setting:

 PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging());

Take notice, though, that Mockito will call the listeners even when you're stubbing methods. Betrachten Sie das folgende Beispiel:

 PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging()); // listeners are called upon encode() invocation when(passwordEncoder.encode("1")).thenReturn("encoded1"); passwordEncoder.encode("1"); passwordEncoder.encode("2");

This snippet will produce an output similar to the following:

 ############ Logging method invocation #1 on mock/spy ######## passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) has returned: "null" ############ Logging method invocation #2 on mock/spy ######## stubbed: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:89) has returned: "encoded1" (java.lang.String) ############ Logging method invocation #3 on mock/spy ######## passwordEncoder.encode("2"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:90) has returned: "null"

Note that the first logged invocation corresponds to calling encode() while stubbing it. It's the next invocation that corresponds to calling the stubbed method.

Andere Einstellungen

Mockito offers a few more settings that let you do the following:

  • Enable mock serialization by using withSettings().serializable() .
  • Turn off recording of method invocations to save memory (this will make verification impossible) by using withSettings().stubOnly() .
  • Use the constructor of a mock when creating its instance by using withSettings().useConstructor() . When mocking inner non-static classes, add an outerInstance() setting, like so: withSettings().useConstructor().outerInstance(outerObject) .

If you need to create a spy with custom settings (such as a custom name), there's a spiedInstance() setting, so that Mockito will create a spy on the instance you provide, like so:

 UserService userService = new UserService( mock(UserRepository.class), mock(PasswordEncoder.class)); UserService userServiceMock = mock( UserService.class, withSettings().spiedInstance(userService).name("coolService"));

When a spied instance is specified, Mockito will create a new instance and populate its non-static fields with values from the original object. That's why it's important to use the returned instance: Only its method calls can be stubbed and verified.

Note that, when you create a spy, you're basically creating a mock that calls real methods:

 // creating a spy this way... spy(userService); // ... is a shorthand for mock(UserService.class, withSettings() .spiedInstance(userService) .defaultAnswer(CALLS_REAL_METHODS));

When Mockito Tastes Bad

It's our bad habits that make our tests complex and unmaintainable, not Mockito. For example, you may feel the need to mock everything. This kind of thinking leads to testing mocks instead of production code. Mocking third-party APIs can also be dangerous due to potential changes in that API that can break the tests.

Though bad taste is a matter of perception, Mockito provides a few controversial features that can make your tests less maintainable. Sometimes stubbing isn't trivial, or an abuse of dependency injection can make recreating mocks for each test difficult, unreasonable or inefficient.

Clearing Invocations

Mockito allows for clearing invocations for mocks while preserving stubbing, like so:

 PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); UserRepository userRepository = mock(UserRepository.class); // use mocks passwordEncoder.encode(null); userRepository.findById(null); // clear clearInvocations(passwordEncoder, userRepository); // succeeds because invocations were cleared verifyZeroInteractions(passwordEncoder, userRepository);

Resort to clearing invocations only if recreating a mock would lead to significant overhead or if a configured mock is provided by a dependency injection framework and stubbing is non-trivial.

Resetting a Mock

Resetting a mock with reset() is another controversial feature and should be used in extremely rare cases, like when a mock is injected by a container and you can't recreate it for each test.

Overusing Verify

Another bad habit is trying to replace every assert with Mockito's verify() . It's important to clearly understand what is being tested: interactions between collaborators can be checked with verify() , while confirming the observable results of an executed action is done with asserts.

Mockito Is about Frame of Mind

Using Mockito is not just a matter of adding another dependency, it requires changing how you think about your unit tests while removing a lot of boilerplate.

With multiple mock interfaces, listening invocations, matchers and argument captors, we've seen how Mockito makes your tests cleaner and easier to understand, but like any tool, it must be used appropriately to be useful. Now armed with the knowledge of Mockito's inner workings, you can take your unit testing to the next level.