
Mockito ist eine Java-Bibliothek um Objekte zu mocken und für mich ein unverzichtbares Werkzeug beim Schreiben von Tests. Ein Mock ist eine Attrappe eines Objektes. Mockito erzeugt ein solches durch Ableitung. Eine Unit mit komplexen Abhängigkeiten lässt sich wunderbar isoliert testen, indem die Abhängigkeiten durch Mocks ersetzt werden.
Da ich zu Mockito viel zu schreiben habe, habe ich mich entschieden, den Artikel in zwei Teilen zu veröffentlichen. Der erste Teil kann über diesen Link erreicht werden. In diesem zweiten Teil gehe ich auf weitere Features von Mockito ein:
- BDDMockito
- JUnit5-MockitoExtension
- ArgumentCaptor
- Answer
BDDMockito
BDDMockito ist eine Klasse, die in der Core-Bibliothek von Mockito mit ausgeliefert wird. BDD steht für Behaviour-driven Development. BDDMockito kommt mit einer API, die mehr zu dem gängigen Test-Pattern von given/when/then passt, als die originalen Mockito-Aufrufe.
given/when/then beschreibt eine Möglichkeit, einen Test zu strukturieren. Dabei wird der Testcode in drei aufeinanderfolgende Abschnitte given, when und then eingeteilt. Im given-Abschnitt werden Inputs und erwartete Outputs deklariert und die Rahmenbedingungen festgesetzt. Der when-Abschnitt besteht oftmals nur aus einer einzelnen Anweisung und beschreibt, was getestet wird. Der then-Abschnitt beinhaltet die Annahmen (assertions) darüber, was im when passiert sein soll. Treffen diese Annahmen nicht zu, schlägt der Test fehl.
BDDMockito ersetzt when()
und then...()
durch given()
und will...()
und verify()
durch then()
, was die Lesbarkeit entsprechend strukturierter Tests verbessert.
Standard-Mockito:
@Test
void shouldDoSmth() {
// given
Car car = mock(Car.class);
when(car.getEngine).thenReturn(mock(Engine.class));
when(car.calcRemainingFuel()).thenThrow(new SensorDefectException());
// when
car.startEngine();
// then
verify(car).accelerate();
verifyNoInteractions(garage);
}
BDDMockito:
@Test
void shouldDoSmth() {
// given
Car car = mock(Car.class);
given(car.getEngine).willReturn(mock(Engine.class));
given(car.calcRemainingFuel()).willThrow(new SensorDefectException());
// when
car.startEngine();
// then
then(car).should().accelerate();
then(garage).shouldHaveNoInteractions();
}
Leider bietet BDD-Mockito kein Gegenstück zu lenient()
, so dass man diese Einstellung entweder am Mock-Objekt selbst oder gar der kompletten Testmethode/-klasse setzen muss, wenn man diese Einstellung für einen Aufruf benötigt.
JUnit5-MockitoExtension
Mockito kann in die Felder einer Klasse über Reflection Mock-Instanzen injizieren. Das ist besonders praktisch, wenn man eine Klasse testen möchte, deren Felder über Dependency Injection gesetzt werden, es funktioniert aber auch für alle anderen Klassen. Um dies zu bewerkstelligen, deklariert man die zu mockenden Abhängigkeiten und die zu testende Instanz in einer Testklasse und weist JUnit per Annotation an, diese mit der JUnit-Mockito-Extension ausführen zu lassen:
@ExtendWith(MockitoExtension.class)
class CarTest {
@Spy
private Engine engine = new Engine();
@Mock
private Horn horn;
@Mock
private Hood hood;
@Mock
private Trunk trunk;
@InjectMocks
private Car car;
// Tests hier einfügen...
}
Für jeden JUnit-Test dieser Testklasse steht nun eine nicht-gemockte Instanz von Car zur Verfügung, deren Felder Engine, Horn, Hood und Trunk mit einem Mock versehen sind. Da die gemockten Felder in der Testklasse bekannt sind, können in den einzelnen Tests ihr Verhalten individuell weiter gemockt und die Interaktionen mit ihnen ausgewertet werden. Die Auflistung der Felder muss nicht vollständig sein, nicht aufgelistete Felder werden in der Instanz, die diese injiziert bekommt, von Mockito einfach ignoriert.
Neben der Mock-Annotation sieht man im Beispiel oben auch ein Feld, das mit Spy annotiert ist. Dabei handelt es sich um eine normale Instanz, also keinen Mock, die von Mockito in einen Wrapper eingebunden wird, so dass über sie trotzdem Annahmen getroffen werden können: verify()
lässt sich auf horn
, hood
und trunk
anwenden, da alle drei Mocks sind. car
hingegen ist eine nicht gemockte Instanz, so dass ein verify(car)
zu einem Fehler führen würde. Die Lösung dafür ist ein Spy
, ein normales Objekt, das aber von Mockito beobachtet wird.
So wie @Mock
das Annotations-Pendant zu der statischen Methode mock()
ist, lässt sich auch ein Spy
über eine statische Methode spy()
instanzieren. Auf Annotationen greife ich immer dann zurück, wenn ich über @InjectMocks
Felder einer zu testenden Instanz mocken möchte.
ArgumentCaptor
Manchmal möchte man verifizieren, dass eine Methode mit einem bestimmten Argument aufgerufen wurde, hat aber keine Referenz auf das Objekt und die Struktur ist zu komplex, als das man sie mit eq()
matchen kann. Wenn man es z.B. mit einer Collection
oder einem Stream
zu tun hat, interessieren einen oftmals nur die Objekte in der Collection
oder dem Stream
, die Reihenfolge der Objekte und die Instanz der Collection
oder des Streams
spielen aber keine Rolle. In so einem Fall kann man auf einen ArgumentCaptor
zurückgreifen. Ein solcher fängt das Argument ab – auch bei wiederholten Aufrufen – und erlaubt es dieses im Nachgang weiter aufzuschlüsseln.
Ein ArgumentCaptor
lässt sich auch über eine statische Methode erzeugen. Ich bevorzuge aber die Annotation-Variante, da sich diese auch typisieren lässt und somit auch gut für Collections
und Streams
geeignet ist:
@ExtendWith(MockitoExtension.class)
class CarTest {
@Spy
private Engine engine = new Engine();
@Mock
private Computer computer;
@InjectMocks
private Car car;
@Captor
private ArgumentCaptor<Stream<SensorData>> captor;
@BeforeEach
private void initMocks() {
MockitoAnnotations.initMocks(this);
}
@Test
void shouldRecordEmissions() {
// given / when
car.accelerate();
// then
verify(computer).collect(captor.capture());
assertThat(captor.getValue())
.extracting("class")
.contains(EmissionData.class);
}
}
Dieser Test demonstriert den Einsatz eines ArgumentCaptors
. Der Test soll aussagen, dass das Auto während der Fahrt Sensordaten an die zentrale Recheneinheit des Fahrzeugs schickt. Unter diesen Sensordaten sollen auch solche enthalten sein, die den Abgasaustritt messen.
Der verwendete ArgumentCaptor
ist vom Typ Stream<SensorData>
und greift die Daten ab, wenn am computer
die Methode collect()
aufgerufen wird, die als Argument einen solchen Stream übergeben bekommt. Über captor.getValue()
kann dieser Stream nun im Nachhinein ausgewertet und in Assertions einbezogen werden.
Damit Mockito den ArgumentCaptor
initialisiert, muss vor Verwendung die statische Methode MockitoAnnotations.initMocks()
mit der Instanz der Testklasse aufgerufen werden.
Answer
Ein weiteres Feature von Mockito ist ...Answer()
. Dabei handelt es sich um eine flexiblere Form von ...Return()
, d.h. thenAnswer()
in Standard-Mockito bzw. willAnswer()
in BDDMockito. Während thenReturn()
einen festen Wert zurückgibt, wird für thenAnswer()
eine anonyme Klasse aufgerufen, in der sich sowohl die Argumente des Aufrufs als auch der Kontext der Testklasse verwenden lassen.
when(car.calcMilesLeftForGivenPetrolRemaining(20, unit))
.thenAnswer(invocation -> invocation.getArgument(0) * 3);
Im vorherigen Beispiel wird für den Aufruf von calcMilesLeftForGivenPetrolRemaining()
immer das Dreifache der angegebenen Menge zurückgegeben. Mit Answer lässt sich aber auch noch weitaus mehr machen, zum Beispiel lässt sich mit einem AtomicInteger
in der Testklasse zählen, um den wievielten Aufruf der Methode es sich handelt. Das ist möglich, da der AtomicInteger
als final
deklariert und dann mit getAndIncrement()
hochgezählt werden kann. In der Kombination mit einer Liste kann dann z.B. das x-te Element aus dieser Liste für die Berechnung der Antwort herangezogen werden. Letztlich kann Answer alles abbilden, was mit einer anonymen Klasse möglich ist.
- Offizielle Webseite von Mockito