Die Konzepte Stubs und Mocks für Unittest am Beispiel Mockito/jUnit
https://oer-informatik.de/java-junit-mockito
tl/dr; (ca. 20 min Lesezeit): Ohne Test-Duplikate ist es oft nicht möglich, ein einzelnes Modul zu testen. Abhängigkeiten machen sonst jeden Unit-Test zum Integrationstest. Wir lernen Mocks und Stubs kennen, und dabei - wie aus Versehen - den Unterschied zwischen zustandsbasierten und verhaltensbasierten Unittests. Am Beispiel Mockito/jUnit…
Abhängigkeiten verhindern unabhängige Tests von Units
Nach dem V-Modell steht jedem Detaillierungsgrad, den wir in der Bearbeitung unseres Produkts haben, eine Testphase gegenüber. Auf der niedrigsten Ebene implementieren wir Module, die wir in Modultests (Unittests) testen. Im Gegensatz zu den Integrationstests der darüberliegenden Ebene sollen hierbei jedoch noch nicht das Zusammenspiel mehrerer Module getestet werden, sondern jede Einheit für sich.

Nicht immer ist es einfach, die einzelnen Units (Module, Klassen, Methoden - je nach Granularität der Tests) zu entkoppeln. Hierbei helfen Mocking-Frameworks. Wir wollen das an einem einfachen Beispiel durchgehen.
Beispiel: Eine Methode testen, die von einem Sensor-Service abhängt
Ausgangslage: Wir haben eine Klasse App, in der eine Methode getInterpretedTemperature()
enthalten ist. Diese Methode wollen wir testen, man nennt das Testobjekt auch System under Test (SUT).

Die Methode getInterpretedTemperature()
interpretiert die von einem Sensor gemessenen Temperaturen als “kalt”, “warm”, etc. und gibt beispielsweise zurück “It’s cold”.
public String getInterpretedTemperature(){
Sensor tempSensor = new Sensor();
Double temp = tempSensor.getTemperatur();
String interpretedTemperature = "";
System.out.println("Interpretierter Wert: "+temp.toString());
if (temp<0){interpretedTemperature = "It's very cold";
}else if (temp<10){interpretedTemperature = "It's cold";
}else if (temp<20){interpretedTemperature = "It's ok";
}else if (temp<30){interpretedTemperature = "It's warm";
}else {interpretedTemperature = "It's hot";
}
return interpretedTemperature;
}
Anstelle einer Sensorwertermittlung per Hardware oder von einem Webservice nutzen wir für unser Beispiel als Sensor
vereinfacht eine normalverteilte Zufallszahl:
import java.util.Random;
class Sensor{
public Double getTemperatur(){
// normalverteilte Zufallszahl als Pseudotemperatur
return new Random().nextGaussian(10, 10);
} }
Das Problem: bei jedem Test von getInterpretedTemperature()
wird auch die Sensormethode tempSensor.getTemperatur()
aufgerufen. Wir könnten also nicht unterscheiden, ob ein Fehler im Sensor auftriff oder im eigentlichen SUT. Zudem kann es unerwünscht (Kundendaten), teuer (Ressourcen) oder unpraktisch (Meldungen, Mails, Ausdrucke) sein, bestimmte Dienste mit jedem Test aufzurufen. Wir müssen also einen Weg finden, dass SUT zu testen, ohne den verknüpften Service aufzurufen.
Der manuelle Weg (ohne Mockito-Framework)
Wenn wir die Testbarkeit auf klassischem Weg herstellen wollen, müssen wir entkoppeln, Objekte extern erzeugen und diese Abhängigkeit injezieren - also den Dreischritt der Dependency Injection gehen. Vorneweg einmal im Schnelldurchlauf, unten dann mit Codebeispielen:
Ein Interface für Sensor erstellen (
Sensierbar
… jaja, ich weiß, der Name…)Referenzen in der App von konkret (
Sensor
) auf abstrakt (Sensierbar
) ändern (das Dependency Inversion Prinzip, DIP, umsetzen)Objekterzeugung externalisieren bzw. Instanzübergabe per Setter ermöglichen (Inversion of Control, IoC).
Sensor
implementiert das neue InterfaceSensierbar
, daneben wird eine neue Klasse (StubSensor
) gebildet, die nur für den Test Sensordaten bereitstellt.Ein Objekt von
StubSensor
während des Tests als Abhängigkeit injezieren (Dependency Injection, DI).
Das Ergebnis sieht im UML-Klassendiagramm dann so aus:

Sensor
-Klasse, StubSensor
-Klasse, die von der AppTest
-Klasse erzeugt und in App
injezieert wird (UML Klassendiagramm)Codeseitig sind die Schritte die folgenden: Das neue Interface (Sensierbar
) muss erstellt werden:
In der Klasse App
müssen die Referenzen auf Sensierbar
angepasst werden und die Instanziierung ausserhalb der Methode getInterpretedTemperature()
vorgenommen werden. Im Beispiel erreichen wir das, indem wir ein Attribut tempSensor
mit einem öffentlichen Setter erstellen und die Sensor-Instanz im Konstruktor erzeugen:
public class App {
Sensierbar tempSensor;
public String getInterpretedTemperature(){
// diese Zeile entfernen: Sensierbar tempSensor = new Sensor();
// Rest wie oben
...}
public App(){
tempSensor = new Sensor();
}
public void setTempSensor(Sensierbar mySensor){
this.tempSensor = mySensor;
}
}
Die Klasse Sensor muss noch erweitert werden - sie implenentiert Sensierbar
:
Zusätzlich benötigen wir noch die eigentliche Mock-Klasse: sie implementiert Sensierbar
und gibt bei Aufruf von getTempertur()
immer den gleichen Wert (13.45) zurück:
Die Vorbereitungen wären abgeschlossen. Jetzt können wir einen jUnit-Testfall erstellen. Maven legt die Ordner unter /test/...
die Ordnerstruktur bereits an, ggf. muss die Testklasse noch erstellt werden (in meinem Fall: AppTest
):
package de.csbme.ifaxx;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
public class AppTest {...}
In der Testklasse kann dann die Testmethode unsere neue StubSensor
-Klasse nutzen: Wir tauschen die Abhängigkeit von der Sensor
-Klasse aus und injizieren das StubSensor
-Objekt. Somit können wir unabhängig von Sensor
testen:
@Test
void testManualStubGetInterpretedTemperature() {
/* given: Preparation */
App myApp = new App(); // Instanz der zu testenden Klasse wird erzeugt
Sensierbar stubSensor = new StubSensor(); // Instanz des Stubsensors
myApp.setTempSensor(stubSensor); // Injezierung des StubSensors
/* when: Execution */
String result = myApp.getInterpretedTemperature();
// der Wert des Stubs wird zurückgegeben: 13.45
/* then: Verification */
String expected = "It's ok"; //gemäß Anforderungen werden 13°C als ok gewertet
assertEquals(expected, result); //Aufruf der Zusicherungsmethode
}
Die Abhängigkeit des System under Test (der App-Methode getInterpretedTemperature();
) vom collaborating service Sensor
wurde auf diesem Weg erfolgreich umgangen - die Methode kann isoliert getestet werden.
Für das Objekt stubSensor
haben wir festgelegt, dass es auf Aufrufe einer Methode mit einem vorgefertigten Wert antworten soll. Es beinhaltet keinerlei weitere Logik. Martin Fowler nennt diese Art Testobjekte einen Stub.
Die Art, wie wir kontrolliert haben, ob der Test erfolgreich verlaufen ist - nämlich in dem wir den Zustand des SUT am Ende mit dem gewünschten Wert verglichen haben - nennt Martin Fowler einen regular test.
Klammer auf: Stub, Mock, Dummy, Fake - ein paar Grundbegriffe
Es ist mit den Namen für Test-Doppelgänger (Mock, Stub, Fake, Dummy) wie bei “Alster” und “Radler” - für manche sind die Begriffe synonym, andere grenzen sie gegeneinander ab. Ich nutze hier die Definitionen von Martin Fowler 1:
Ein Dummy wird nie selbst genutzt und dient nur als Platzhalter.
Ein Fake enthält etwas vereinfachte Implementierung, um eine teure Operation zu simulieren (z.B. unsere Sensor-Klasse statt eines tatsächlichen Sensors oder einfache In-Memory-Listen statt Datenbankzugriffe).
Ein Stub-Objekt enthalten zwar keinerlei Logik, antwortet auf Aufrufe aber mit definierten konstanten für Tests brauchbaren Rückgabewerten. In obigem Beispiel ist
StubSensor
eine Klasse für solche Objekte.
A Test Stub is an “test-specific object that feeds the desired indirect inputs into the system under test”.2
- Und Mock-Objekte? In Mock-Objekten ist neben den vorgegebenen Antworten auch noch festgelegt, welche Aufrufe dieses Objekts erwartet werden. Mit Mock-Objekten ist es also möglich, nicht nur den Zustand des SUT nach dem Test zu verifizieren, sondern auch das Verhalten des SUT während des Tests (durch Verifizierung der Aufrufe des Mock-Objekts).
Replace an object the system under test (SUT) depends on with a test-specific object that verifies it is being used correctly by the SUT. 3
Implizit haben wir soeben zwei unterschiedliche Ansätze des Unit-Testens kennengelernt:
Regular Tests: Der Zustand des Testobjekts (SUT - System under Test) wird am Ende mit Assert überprüft. (Fowler nennt den Ansatz classical, einige bezeichnen es als die Detroiter Unit-Test-Schule)
Behavioral Test: Beim Testen mit Mock-Objekten wird nicht nur der Zustand des SUT am Ende verglichen, sondern auch das Verhalten des SUT. Fowler spricht hier vom mockist TDD-Ansatz, der Londoner Unit-Test-Schule. Hierauf baut auch das Behavioral Driven Development auf.
Regular Tests: Stubs mit Mockito
Wenn wir das Testframework Mockito nutzen, können wir uns einen guten Teil der Arbeit sparen: wir müssen kein Interface mehr erstellen und keine Stubklasse
implementieren. Das Stubobjekt
wird direkt durch das Framework per
erstellt und Verhalten und die vorgeschriebene Antwort per
injeziert. mock
und when
sind dabei statische Methoden der Klasse Mockito
und müssen importiert werden.
Der Rückgabewert von when
ist vom Typ OngoingStubbing
. Die Methoden, die gegen dieses Interface implementiert werden, sind wiederum vom Typ OngoingStubbing
, weshalb sie verkettet werden können (“Fluent API”). Es könnte also z.B. festgelegt werden, dass beim ersten Aufruf 1, beim zweiten 2, beim dritten 4 und beim vierten eine Exception vom Stub zurückgegeben wird:
when(mockSensor.getTemperatur())
.thenReturn(1.0)
.thenReturn(2.0)
.thenReturn(4.0);
.thenThrow(new NullPointerException());
Das UML-Klassendiagramm sieht für den uns bislang bekannten Teil der Klasse Mockito
und des Interfaces OngoingStubbing
etwa so aus:
Bis auf diese Anpassungen bleibt der Rest der Testmethode wie beim manuellen Beispiel oben:
@Test
void testGetInterpretedTemperature() {
/* given: Preparation */
App myApp = new App();
Sensor mockSensor = mock(Sensor.class);
when(mockSensor.getTemperatur()).thenReturn(12.345); // Use mock as stub
myApp.setTempSensor(mockSensor);
/* when: Execution */
String result = myApp.getInterpretedTemperature();
/* then: Verification */
String expected = "It's ok";
assertEquals(expected, result);
}
Im Code schön zu sehen: in der Verification-Phase des Tests betrachten wir nur den Zustand des SUT. Das wollen wir im folgenden erweitern.
Behavioral Tests mit Mockito
Jetzt zünden wir die zweite Stufe: wir haben bei der Mock/Stub-Definition von Martin Fowler gelernt, dass Mock-Objekte erlauben, nicht nur den Zustand des SUT, sondern auch dessen Verhalten zu beobachten.
Um das einmal beispielhaft auszuführen fügen wir eine Anforderung an unsere SUT Methode an: Sensorergebnisse sollen während der Laufzeit zwischengespeichert werden. Ein Aufruf von myApp.getInterpretedTemperature()
soll nur dann den Sensor abfragen, wenn kein zwischengespeicherter Wert vorliegt.
Als kleiner Zwischenschritt: Mit Hilfe der Methode verify()
können wir im Rahmen der Verifizierungsphase im Test prüfen, ob die Methode des Mock-Objekts im Rahmen des Tests wirklich aufgerufen wurde (das einfachste Verhalten). Dazu müssen wir nur am Ende des Tests die folgende Zeile anfügen:
Wenn jetzt ein böswilliger Entwickler die Logik unserer Methode myApp.getInterpretedTemperature()
umbaut, die Sensorabfrage auskommentiert und immer 15.0 zurückgibt:
Werden alle zustandsbasierten Tests weiterhin bestanden. Die verify()
-Zeile oben wird unseren verhaltensorientierten Test aber scheitern lassen: die Methode wird nie aufgerufen. Ein erster Schritt. Aber wie bekommen wir heraus, wie oft unser Mock aufgerufen wurde? Wir wollen ja ab dem ersten Aufruf einen gecachten Wert nutzen.
Um den Cache zu implementieren ändern wir unser Klasse App
: ein neues Attribut cachedTemp
, dass im Konstruktor auf einen Defaultwert gesetzt wird und in unserer SUT
-Methode genutzt/befüllt wird:
public class App {
...
Double cachedTemp;
...
public String getInterpretedTemperature(){
Double temp;
if (cachedTemp != -Double.MAX_VALUE){
temp = cachedTemp;
}else{
temp = tempSensor.getTemperatur();
cachedTemp = temp;
}
...
}
public App(){
tempSensor = new Sensor();
cachedTemp = -Double.MAX_VALUE;
}
...
}
Mockito bietet neben verify()
auch noch weitere Möglichkeiten, das Verhalten zu überprüfen, es gibt eine Reihe weiterer statischer Methoden. In unserem Fall ist die überladene Methode verify(mock, VerificationMode)
interessant.

Wir können über den zweiten Parameter übergeben, wie häufig eine Methode unseres Mock-Objekts aufgerufen werden soll. Wie das im einzelnen aussieht, dazu später. Jetzt wollen wir erstmal die Testmethode erstellen:
Wir rufen zweimal hintereinander unsere SUT-Methode auf. Als Zustand erwarten wir (in der Verification-Phase), dass alle den gleichen Wert “It’s cold” zurück liefern. Als Verhalten erwarten wir (verify()
), dass die Mock-Methode höchstens einmal (atMost(1)
) aufgerufen wird:
@Test
void testCachedGetInterpretedTemperature() {
/* given: Preparation */
App myApp = new App();
Sensor mockSensor = mock(Sensor.class);
when(mockSensor.getTemperatur()).thenReturn(12.345); // Use mock as stub
myApp.setTempSensor(mockSensor);
/* when: Execution */
String result = myApp.getInterpretedTemperature();
String result2 = myApp.getInterpretedTemperature();
/* then: Verification */
String expected = "It's ok";
assertEquals(expected, result);
assertEquals(expected, result2);
verify(mockSensor, atMost(1)).getTemperatur();
}
Die genutzten Bedingungen an das Verhalten des Mock-Objekts werden durch Implementierungen des Interfaces VerificationMode
überprüft. Die Klasse Mockito
nutzt solche Implementierungen als Parameter der Methode verify()
und erzeugt sie auch selbst in den statischen Factory-Methoden wie atMost()
. Die Mockito
-Klasse ist selbst in dem folgenden Beispiel schon unübersichtlich. Trotzdem lohnt sich ein Blick auch in das zugehörige _Javadoc-File: https://javadoc.io/doc/org.mockito/mockito-all/latest/index.html
Mit Hilfe dieses Tests können wir nun überprüfen, ob das Caching funktioniert.
Weitere Funktionalität von Mockito
Wir haben bislang nur einen verify()
Aufruf genutzt. Wenn wir unterschiedliche Methodenaufrufe unseres Mock-Objekts überprüfen wollen, können wir diese einfach nacheinander aufführen. Wir erweitern unseren Sensor-Service (und dessen Interface) um eine zweite Methode: ein Attribut offsetTemperature
mitsamt Setter:
class Sensor implements Sensierbar{
Double offsetTemperature = 0.0;
public void setOffsetTemperature(Double temp){
this.offsetTemperature = temp;
}
public Double getTemperatur(){...}
}
Dieser Setter wird vor jeder Messung im SUT (getInterpretedTemperature()
) auf 0.0 gesetzt:
public String getInterpretedTemperature(){
...
tempSensor.setOffsetTemperature(0.0);
temp = tempSensor.getTemperatur();
...}
In der Testmethode prüfen wir in der Verification-Phase:
ob der Setter (überhaupt) mit einem beliebigen
Double
-Wert (anyDouble()
) aufgerufen wurde,ob der .getTemperatur(); (überhaupt) aufgerufen wurde,
ob alle Aufrufe des Mockobjekts verifiziert wurden, oder ob es über die vorgenannten hinaus noch weitere Mockmethoden-Aufrufe gab.
verify(mockSensor).setOffsetTemperature(anyDouble());
verify(mockSensor).getTemperatur();
verifyNoMoreInteractions(mockSensor);
Die oberen beiden Zeilen können getauscht werden, es wird also intern nur “abgehakt”, welche Methoden aufgerufen wurden. Am Ende wird geschaut, ob alle Methodenaufrufe einen imaginären Haken haben. Falls Methoden mehrfach aufgerufen werden, muss dies mit den oben gezeigten VerificationModes
angezeigt werden.
Was man an diesem Beispiel auch sieht: anyDouble()
dient als ArgumentMatcher
- ein komfortabler Weg, wenn man nicht präzise sagen kann, welche Argumente an das Mock-Objekt übergeben werden. Es gibt davon einige - das betroffene Javadoc zu ArgumentMatchers
hilft hier gerne.
Bei dem obigen Beispiel spielte die Reihenfolge, in der die verify()
-Aufrufe erfolgten keine Rolle, es wurde nur “abgehakt”, was aufgerufen wurde. Wenn auch die Aufrufreihenfolge relevant ist, kommt mit der InOrder
-Klasse ein weiteres Feature von Mockito ins Spiel: Folgende Verifikation würde fehlschlagen, da der Aufruf im SUT in anderer Reihenfolge erfolgt:
InOrder inOrder = Mockito.inOrder(mockSensor, mockSensor);
inOrder.verify(mockSensor).getTemperatur();
inOrder.verify(mockSensor).setOffsetTemperature(anyDouble());
verifyNoMoreInteractions(mockSensor);
Die Instanziierung der Mock-Objekte, die wir oben in jedem Test mit der Zeile Sensor mockSensor = mock(Sensor.class);
ausgeführt haben, kann Mockito auch automatisch erstellen für alle Attribute in der Testklasse, die mit @Mock
annotiert sind.
Damit der Testrunner von jUnit, der die eigentlichen Tests ausführt, diese annotierten Attribute auch findet, muss die Testklasse wiederum auch annotiert werden mit: @ExtendWith(MockitoExtension.class)
.
Ein Beispiel mit den nötigen Imports, der auskommentierten alten Instanziierung und dem Mock-Objekt als Attribut sieht gekürzt so aus:
import org.mockito.Mock;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class AppTest {
@Mock
Sensor mockSensor;
@Test
void testGetInterpretedTemperature() {
/* given: Preparation */
App myApp = new App();
//Sensor mockSensor = mock(Sensor.class);
...
} }
Mockito bietet noch unglaublich viele weitere Möglichkeiten wie zum Umgang mit Exceptions, die Verifizierung regulärer Objekte mit Spy usw. - ein Blick in die Framework-Javadoc-Seite von Mockito umreist die Möglichkeiten dieses Test-Tools.
Fazit
Wir haben bislang nur sehr an der Oberfläche von Mockito gekratzt, aber dabei zwei wesentliche Konzepte kennengelernt:
Das zustandsbasierte Testen mit Hilfe von Stubs auf der einen und
das verhaltensbasierte Testen mit Mocks auf der anderen Seite.
Sehr häufig wird Testen nur auf die erste Technik beschränkt und damit die Möglichkeiten verspielt, die ein Mocking-Framework wie Mockito überhaupt erst bietet.
Es lohnt sich, an einem Beispiel die jeweiligen Möglichkeiten dieser zwei Arten zu testen auszuloten - und sie fortan gezielt und passend einzusetzen.
Links und weitere Informationen
Alte, aber sehr lesenswerte Seite mit Hintergründen zu Unittest, Mock, Stubs usw.: xunitpatterns.com
Gutes Tutorial von Vogella zu iUnit/Mockito
Gutes Tutorial von Dzone zu Mockito/jUnit
Quellen und offene Ressourcen (OER)
Die Ursprungstexte (als Markdown), Grafiken und zugrunde liegende Diagrammquelltexte finden sich (soweit möglich in weiterbearbeitbarer Form) in folgendem git-Repository:
http://gitlab.com/oer-informatik/db-sql/dbms.
Sofern nicht explizit anderweitig angegeben sind sie zur Nutzung als Open Education Resource (OER) unter Namensnennung (H. Stein, oer-informatik.de) freigegeben gemäß der Creative Commons Namensnennung 4.0 International Lizenz (CC BY 4.0).
Martin Fowler bezieht sich in seinem Artikel (“Mocks Aren’t Stubs”, Martin Fowler 02 January 2007) in der Wahl der Bezeichnungen auf ein Buch von Gerard Meszaros - das liegt mir aber nicht vor↩
Bei xunitpatterns findet sich eine genauere Definition eines Stub-Objekts↩
Bei xunitpatterns findet sich eine genauere Definition eines Mock-Objekts↩