Java Mocking mit Mockito
Abhängigkeiten in Java-Tests mit Mockito mocken – mock, when/thenReturn, verify und ArgumentCaptors.
Ein Unit-Test sollte eine Klasse isoliert testen. Aber echte Klassen sind auf Mitarbeiter angewiesen — eine Datenbank, ein Zahlungsgateway, ein E-Mail-Versender — die langsam, unzuverlässig oder mit Nebeneffekten behaftet sind, die man in einem Test nicht haben möchte. Mockito ist die am weitesten verbreitete Java-Bibliothek, um diese Mitarbeiter durch Mocks zu ersetzen: Ersatzobjekte, die man so programmiert, dass sie vordefinierte Antworten zurückgeben, und die man anschließend darüber befragen kann, wie sie aufgerufen wurden. Dieses Kapitel zeigt die Mockito-API, die man täglich schreibt, und belegt die zugrunde liegende Idee mit einem reinen JDK-Programm, das man direkt hier ausführen kann.
Dieses Kapitel setzt voraus, dass man die in JUnit 5 Einführung und JUnit Assertions behandelten Test-Grundlagen bereits kennt. Mockito ergänzt JUnit — JUnit führt den Test aus und prüft Werte, während Mockito die gefälschten Mitarbeiter bereitstellt.
Warum überhaupt mocken
Die zu testende Klasse (das System under Test, oder SUT) erhält ihre Mitarbeiter in der Regel über ihren Konstruktor — das ist der Vorteil von Dependency Injection. Im Test übergibt man ihr einen gefälschten Mitarbeiter anstelle des echten. Ein guter Fake erfüllt zwei Aufgaben:
- Stubbing — er gibt den Wert zurück, den das Testszenario benötigt (
charge(...)gibttruezurück oder wirft eine Ausnahme), sodass man das SUT ohne echten Netzwerkaufruf auf einen bestimmten Pfad führen kann. - Verifizierung — er zeichnet jeden erhaltenen Aufruf auf, sodass der Test danach bestätigen kann, dass das SUT ihn auf die richtige Weise, die richtige Anzahl von Malen und mit den richtigen Argumenten aufgerufen hat.
Mockito erzeugt zur Laufzeit einen solchen Fake für jedes Interface oder jede nicht-finale Klasse, sodass man nie einen handschriftlich erstellen muss. Zu wissen, was es generiert, macht die API jedoch offensichtlich.
Mocks erstellen und Rückgaben stubben
Mockito.mock(Type.class) erzeugt einen Mock. Standardmäßig gibt jede Methode einen „netten" leeren Wert zurück — null für Objekte, false für boolean-Werte, 0 für Zahlen. Anschließend überschreibt man die relevanten Methoden mit 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"));Für void-Methoden dreht sich die Reihenfolge um: doThrow(...).when(mock).method(). Stubs können auch mit Argument-Matchern wie anyString() und anyInt() gelockert werden, sodass sie bei jedem Aufruf ausgelöst werden, nicht nur bei einem bestimmten Satz von Argumenten.
Interaktionen verifizieren
Nachdem das SUT ausgeführt wurde, bestätigt verify(...), wie der Mock verwendet wurde. So testet man Nebeneffekte — eine E-Mail, die hätte gesendet werden sollen, eine Zeile, die hätte gespeichert werden sollen — ohne das echte System zu untersuchen.
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 happenedDie gängigen Verifizierungsmodi:
| Modus | Bedeutung |
|---|---|
times(n) | Genau n-mal aufgerufen |
never() | Entspricht times(0) |
atLeastOnce() / atLeast(n) | Mindestens einmal / n-mal aufgerufen |
atMost(n) | Höchstens n-mal aufgerufen |
only() | Dies war die einzige Methode, die auf dem Mock aufgerufen wurde |
Argumente erfassen
Wenn man prüfen möchte, was übergeben wurde — nicht nur, dass ein Aufruf stattgefunden hat — verwendet man einen ArgumentCaptor. Er greift das tatsächliche Argument auf, sodass man auf seine Felder prüfen kann. Dies ist äußerst wertvoll, wenn das SUT ein Objekt aufbaut, bevor es weitergegeben wird.
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 und Spies
In echten Testklassen ruft man mock() selten manuell auf. Die Annotationen verdrahten alles: @Mock deklariert ein Mock-Feld, @InjectMocks erstellt das SUT und schiebt die Mocks in seinen Konstruktor, und @ExtendWith(MockitoExtension.class) (JUnit 5) aktiviert die Verarbeitung.
@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);
}
}Ein Spy (spy(realObject)) ist der Mittelweg: er umhüllt ein echtes Objekt und führt echte Methoden aus, sofern diese nicht gestubbt werden — praktisch für das partielle Mocken von Legacy-Code.
final-Klassen, final-Methoden, static-Methoden oder private-Methoden mocken. Wenn man eine final-Klasse mocken muss, aktiviert man den mockito-inline-MockMaker; andernfalls sollte man hin zu einem Interface refaktorieren.Wann man nicht mocken sollte
Mocks sind mächtig, aber übermäßiges Mocken erzeugt Tests, die bestehen, während der echte Code fehlerhaft ist. Man greift zu einem Mock nur dann, wenn der echte Mitarbeiter langsam, nicht-deterministisch, mit Nebeneffekten behaftet oder noch nicht gebaut ist. Man sollte nicht Value-Objekte, die zu testende Klasse selbst oder Typen mocken, die man nicht besitzt (stattdessen eine Drittanbieter-API in das eigene Interface einwickeln und dieses mocken). Wenn der Mitarbeiter günstig und rein ist — ein einfacher Taschenrechner, eine In-Memory-Liste — verwendet man das Echte und prüft direkt auf dessen Ergebnis.
Ein ausgearbeitetes Beispiel: ein handgeschriebener Mock
Mockito selbst ist auf dem Classpath dieser Seite nicht vorhanden, daher erstellt das ausführbare Programm unten den Mock von Hand — eine kleine Klasse, die das Abhängigkeits-Interface implementiert, einen gestubbten Rückgabewert hält und jeden Aufruf aufzeichnet. Das ist genau die Mechanik, die Mockito zur Laufzeit für einen generiert, sodass man beim Lesen genau versteht, was when/thenReturn und verify im Hintergrund tun.
Was aus dem Lauf zu entnehmen ist:
- Das
stubbedResult = truedesMockGatewayist die handgeschriebene Form vonwhen(gateway.charge(...)).thenReturn(true); weil der Stubtruezurückgab, druckte das SUTresult : PAIDaus, ohne dass jemals eine echte Zahlung stattfand. invocationCount == 1gibttrueaus und entspricht genau dem, wasverify(gateway).charge(...)prüft — der Mock hat gezählt, dass er einmal aufgerufen wurde, und das ist es, womit Mockito „Hat diese Interaktion stattgefunden?" in eine Bestehen/Nicht-Bestehen-Aussage umwandelt.- Die
calls-Liste hatcharge(acct-7, 1999)erfasst, die Argument-Capture-Idee hinterArgumentCaptor: Ein Mock erinnert sich nicht nur daran, dass er aufgerufen wurde, sondern auch womit, sodass der Test auf die tatsächlichen Argumente prüfen kann. - Das Neu-Erstellen des Mocks mit
stubbedResult = falseführte das SUT auf seinen anderen Zweig und drucktedeclined result : DECLINEDaus, was zeigt, wie ein Fake es ermöglicht, jedes Szenario zu skripten, das der echte Mitarbeiter erzeugen könnte. - Die Guard-Klausel gab
INVALIDzurück, bevor das Gateway erreicht wurde, sodassinvocationCount == 0den Werttrueausgab — der ausführbare Beweis vonverify(gateway, never()).charge(...), der bestätigt, dass eine Abhängigkeit absichtlich nicht berührt wurde.