Mocks, Stubs, Fakes, Dummies und Test Doubles – Anwendungsentwickler-Podcast #143

Mocks, Stubs, Fakes, Dummies und Test Doubles – Anwendungsentwickler-Podcast #143

Um Möglichkeiten, Abhängigkeiten in Tests loszuwerden, geht es in der einhundertdreiundvierzigsten Episode des Anwendungsentwickler-Podcasts. Inhalt Vorweg: Automatisierte Tests gibt es nicht nur für objektorientierte Software,
60 Minuten

Beschreibung

vor 6 Jahren

Um Möglichkeiten, Abhängigkeiten in Tests loszuwerden, geht es in
der einhundertdreiundvierzigsten Episode des
Anwendungsentwickler-Podcasts.
Inhalt

Vorweg: Automatisierte Tests gibt es nicht nur für
objektorientierte Software, sondern natürlich auch für
funktionale, prozedurale usw. Die folgenden Inhalte beziehen sich
aber ausschließlich auf die Objektorientierung. In anderen
Paradigmen haben die genannten Begriffe evtl. andere Bedeutungen
oder die vorgestellten Lösungen funktionieren etwas anders, da es
z.B. keine Polymorphie gibt.
Grundlagen


Automatisierte Tests sollen das Verhalten
unseres Systems prüfen und nur fehlschlagen, wenn ein Fehler im
Code vorliegt. Sie sollen schnell und wiederholbar sein, damit
sie so oft wie möglich ausgeführt werden. Sie sollen immer und
überall (auf allen Rechnern/Umgebungen) ausführbar sein.


Unit-Tests prüfen das Verhalten einer
einzelnen Komponente, z.B. eine Methode, in Isolation.


Integrationstests prüfen das Verhalten
mehrerer Komponenten, z.B. Objekte, im Zusammenspiel.

Integrationstests werden auch Tests genannt, die die
Infrastruktur berühren, also z.B. eine Datenbank, das
Netzwerk oder das Dateisystem.

Die Infrastruktur sollte in Tests nicht
berührt werden, da diese schnell Fehler produziert: erwartete
Datenbankinhalte können sich ändern, im Dateisystem fehlen
Berechtigungen oder das Netzwerk ist nicht verfügbar.

Die Isolation von Komponenten ist schon in
kleinen Systemen nicht immer einfach. Ein Objekt kann seine
Aufgaben fast nie komplett allein erledigen, sondern braucht
andere Objekte dafür. Ein Service braucht vielleicht ein
Repository, um die zu verarbeitenden Daten aus der Datenquelle zu
lesen. Ist kein Repository vorhanden, gibt es vielleicht eine
NullPointerException beim Aufruf der zu testenden Methode.

Diese Abhängigkeiten machen die Tests
schwierig, da das zu testende Objekt nicht korrekt funktioniert,
wenn sie nicht vorhanden sind. Somit müssen alle für den
konkreten Test benötigten Abhängigkeiten durch diesen
bereitgestellt werden.

Somit enthält ein Test nicht nur die eigentlich zu testende
Komponente, sondern auch noch ihre Abhängigkeiten. Damit klar
ist, welche der verschiedenen Komponenten nun eigentlich getestet
werden soll, bekommt sie die Bezeichnung System under
test (abgekürzt SUT).

Beim Test können grundsätzlich die „echten“ Komponenten
verwendet werden, falls dies möglich und sinnvoll ist. Oder die
Komponenten werden durch sog. Test Doubles
ersetzt, wie ein Stuntdouble den eigentlichen Schauspieler
ersetzt.

Test Doubles


Test Doubles sind der Oberbegriff für
Komponenten, die in Tests verwendet werden, um die
Abhängigkeiten des SUT zu ersetzen. Sie sollen vor allem für
vorhersagbare Testergebnisse sorgen, indem
z.B. immer die gleichen Werte aus dem Speicher zurückgegeben
werden und nicht potentiell unterschiedliche Werte aus der
Datenbank gelesen werden.

Damit das Ganze funktioniert, müssen die echten Komponenten
durch die Test Doubles ersetzt werden können. In
objektorientierter Software kommt dabei die
Polymorphie zum Einsatz. Die Abhängigkeiten
müssen also z.B. als Interface oder als (abstrakte) Basisklasse
vorliegen, damit die Test Doubles anstelle der echten Komponenten
genutzt werden können.

Außerdem ist es nötig, dass die Test Doubles dem SUT
„untergejubelt“ werden können. Es ist also irgendeine Form von
Dependency Injection nötig, z.B.
Konstruktorparameter oder Setter-Methoden. Sobald das SUT sich
selbst seine Abhängigkeiten erzeugt (z.B. mit new), ist ein Test
mit Test Doubles schwierig oder gar unmöglich.

Das alles hat auch eine Auswirkung auf den
Produktivcode. Denn wenn das SUT eine
Abhängigkeit als Konstruktorparameter übergeben bekommen muss,
wird auch der Produktivcode die echte Komponente so hineingeben
müssen.

Die Tests haben somit indirekt zur Folge, dass der Code
insgesamt modularer wird, was die Softwarequalität erhöht.

Zum Erstellen von Test Doubles gibt es verschiedene
Frameworks, z.B. Mockito in Java oder Moq für .NET.

Fakes


Fakes (engl. fake = Fälschung, Imitation)
können ohne Framework einfach selbst implementiert werden.

Ihre Implementierung ähnelt der echten, ist aber z.B.
einfacher/schneller oder gibt nur harte Werte zurück.

Beispiel: InMemory-Datenbank statt einer echten verwenden.

Dummy


Dummies (engl. dummy = Attrappe, Strohmann)
sind Platzhalter, deren Funktion im Test eigentlich gar nicht
benötigt wird.

Sie werden verwendet, um den Compiler zufriedenzustellen, da
z.B. ein Objekt als Parameter erwartet wird.

Wenn die Funktionalität wirklich überhaupt nicht verwendet
(=aufgerufen) wird, kann auch null verwendet werden.

Stubs


Stubs (engl. stub = Stummel, Stumpf) geben auf
Anfragen definierte (=harte) Werte zurück, um das Verhalten des
SUT vorhersagbar zu machen oder teure und fehleranfällige
Zugriffe auf die Infrastruktur zu vermeiden. Außerdem dienen
sie dazu, ansonsten schwer zu produzierende Zustände
abzubilden, z.B. das Werfen einer Exception.

Stubs werden für in das SUT eingehende Daten
verwendet.

Beispiel: Ein Repository gibt dem SUT immer den gleichen
Datensatz zurück, ohne auf die Datenbank zuzugreifen.

Das Verhalten kann parametrisiert werden, z.B. für ID 1 ein
bestimmter Datensatz und für andere IDs eine Exception.

Beispiel in Mockito: when(repo.getUser(1)).thenReturn(new
User("Stefan"));

Einsatzgebiete: Dateisystem, DB, Netzwerk usw.

Teilt man die Methodenaufrufe seines Systems in
Queries (nur lesen, keine Zustandsänderung,
Rückgabewert) und Commands (Zustandsänderung,
kein Ergebnis als Rückgabewert) auf, verwendet man Stubs für die
Queries.

Die Tests verwenden „normale“ Assertions, um
das Ergebnis des SUT zu prüfen (assert in JUnit).

Mocks


Mocks (engl. mock = Fäschung, Nachahmung)
„merken“ sich die Methodenaufrufe an ihnen und können im
Nachhinein verifizieren, ob ein Methodenaufruf stattfand, wie
oft und mit welchen Parametern.

Mocks werden für aus dem SUT ausgehende
Befehle verwendet.

Beispiel: Das SUT soll eine Mail verschicken und dafür am
MailServer die Methode send() mit bestimmten Parametern (z.B.
Adresse, Betreff) aufrufen.

Oftmals müssen die Mocks auch Daten zurückgeben, damit das
SUT funktioniert. Eigentlich sollten sie das aber nicht tun. Dies
weist auf eine Vermischung von Command und Query hin.

Beim Command-Query-Pattern, verwendet man Mocks für die
Commands.

Die Tests verwenden keine Assertions gegen das SUT, sondern
prüfen am Mock, ob die richtigen Methoden
aufgerufen wurden (verify in Mockito).

Beispiel in Mockito: verify(mailServer).send("stefan@macke",
"Hallo Stefan!");

Spy


Spies (engl. spy = Spion) sind nicht eindeutig
definiert.

Ein Spy kann ein Stub mit „Aufzeichnungsfunktion“ der
Interaktionen (ähnlich zum Mock) sein (siehe Test Double).

In Mockito ist ein Spy eine Art Mock zur Aufzeichnung der
Interaktionen, aber mit der Möglichkeit der Delegation der
Aufrufe an die echte Komponente (siehe Spy). Der
Spy „umschließt“ also das echte Objekt, kann einzelne Methoden
überschreiben und delegiert den Rest an das echte Objekt. Im
Nachhinein kann dann noch geprüft werden, welche Methoden
aufgerufen wurden.

Vor- und Nachteile von Test Doubles

Vorteile

Tests sind nicht abhängig von änderungsanfälliger
Infrastruktur.

Tests sind einfacher, da keine komplexe Infrastruktur
aufgebaut werden muss.

Tests lassen sich jederzeit und überall wiederholbar
durchführen.

Tests sind schneller, da keine Infrastruktur berührt
wird.

Der Code wird modularer und Abhängigkeiten werden
offensichtlich.



Nachteile

Das Zusammenspiel der „echten“ Komponenten wird nicht
getestet. Es sind zusätzliche Integrationstests nötig.

Das einfache Erstellen von Test Doubles mit Frameworks
führt ggfs. zu komplexen Test-Setups oder Overengineering.



Allgemeine Hinweise und Empfehlungen

Viele Frameworks (u.a. Mockito) unterscheiden nicht zwischen
Stub und Mock. Die erzeugten Test Doubles können sowohl feste
Ergebnisse liefern als auch die Interaktion mit ihnen
aufzeichnen. Die Unterscheidung liegt also allein darin, wie das
Test Double im Test verwendet wird.

Grundsätzlich sollte immer die echte Implementierung im Test
bevorzugt werden, bevor Test Doubles genutzt werden, da somit
gleich mehrere „echte“ Komponenten des Systems mitgetestet werden
und insb. auch deren Interaktion.

Zugriffe auf die Infrastruktur, die die Tests langsam und
fehleranfällig machen, sollten immer durch Test Doubles ersetzt
werden.

Tests, die ausschließlich mit Test Doubles arbeiten, reichen
nicht aus, um die Funktionalität des Gesamtsystems zu
gewährleisten. Es sind dann weitere Integrationstests mit den
echten Komponenten nötig.

Wenn das Setup der Test Doubles zu umständlich wird (z.B.
Doubles, die Doubles zurückgeben, die Doubles zurückgeben),
sollte man das Design seiner Komponenten überdenken (z.B. Law of
Demeter).

Literaturempfehlungen

Zum Einstieg in Unit-Tests inkl. Mocking kann ich sehr Pragmatic
Unit Testing in Java 8 with JUnit* von Jeff Langr empfehlen. Das
Buch lesen meine Azubis bereits im 1. Ausbildungsjahr und ich
habe auch schon eine Podcast-Episode dazu aufgenommen: Pragmatic
Unit Testing in Java 8 with JUnit (Buchclub).


*
Links

Permalink zu dieser Podcast-Episode

RSS-Feed des Podcasts

Test Double

Mocks Aren’t Stubs

Test Doubles — Fakes, Mocks and Stubs.

Kommentare (0)

Lade Inhalte...

Abonnenten

15
15