W3docs

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(...) gibt true zurü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 happened

Die gängigen Verifizierungsmodi:

ModusBedeutung
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.

Warnung
Mockito kann nur mocken, was überschreibbar ist. Standardmäßig kann es keine 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.

java— editable, runs on the server

Was aus dem Lauf zu entnehmen ist:

  • Das stubbedResult = true des MockGateway ist die handgeschriebene Form von when(gateway.charge(...)).thenReturn(true); weil der Stub true zurückgab, druckte das SUT result : PAID aus, ohne dass jemals eine echte Zahlung stattfand.
  • invocationCount == 1 gibt true aus und entspricht genau dem, was verify(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 hat charge(acct-7, 1999) erfasst, die Argument-Capture-Idee hinter ArgumentCaptor: 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 = false führte das SUT auf seinen anderen Zweig und druckte declined result : DECLINED aus, was zeigt, wie ein Fake es ermöglicht, jedes Szenario zu skripten, das der echte Mitarbeiter erzeugen könnte.
  • Die Guard-Klausel gab INVALID zurück, bevor das Gateway erreicht wurde, sodass invocationCount == 0 den Wert true ausgab — der ausführbare Beweis von verify(gateway, never()).charge(...), der bestätigt, dass eine Abhängigkeit absichtlich nicht berührt wurde.

Übung

Übung
Was ist in einem Mockito-basierten Unit-Test der Zweck eines Aufrufs wie verify(gateway, never()).charge(anyString(), anyInt())?
Was ist in einem Mockito-basierten Unit-Test der Zweck eines Aufrufs wie verify(gateway, never()).charge(anyString(), anyInt())?
Was this page helpful?