Mocks, Stubs und andere Test Doubles

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.

Probeabo bei Audible (Affiliate)

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

Jeff Langr - Pragmatic Unit Testing in Java 8 with JUnit (Affiliate)*

Links

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax