Um Möglichkeiten, Abhängigkeiten in Tests loszuwerden, geht es in der einhundertdreiundvierzigsten Episode des Anwendungsentwickler-Podcasts.
Podcast: Play in new window | Download (Duration: 59:57 — 28.6MB)
Abonnieren: Apple Podcasts | Spotify | RSS
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 einRepository
, um die zu verarbeitenden Daten aus der Datenquelle zu lesen. Ist keinRepository
vorhanden, gibt es vielleicht eineNullPointerException
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 Methodesend()
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.
Finde ich toll, dass du mit deinen Azubis so gruendlich arbeitest und ihr euch sogar mit solchen Buechern befasst. Aber findest du es wirklich fair soetwas in der Pruefung zu fragen? Zumindest haben ich bei mir in der Schule wirklich NIE auch nur annaehernd ueber das Konzept von Tests, geschweige denn Mocks/Strubs/… oder sinnvolle Strukturierung, gesprochen.
Und vom Betrieb aus hatte ich einfach nur das Verstaendnis: Mocken ueberschreibt die Komplette Klasse, rausmocken was nicht geht, machen bis es passt. Ist das toll? Nein, so war es aber eben.
Ich hatte erst nach der Ausbildung mich wirklich damit befasst und es besser verstanden.
Also wie gesagt, versteh mich nicht falsch, du scheinst ja wirklich super mit deinen Azubis zu arbeiten und ich wuerde mir auch sehr wuenschen ueber solche Themen in der Schule zu sprechen, aber wie kannst du es verantworten, soetwas in der Pruefung zu fragen?
Hallo Nunya, ja ich finde das „fair“. Denn Unit-Tests gehören meiner Meinung nach zum Grundwissen jedes Entwicklers. Nicht umsonst sind sie als expliziter Punkt in der neuen Berufsverordnung hinzugekommen. Wir fragen häufig Dinge, die nicht in der Berufsschule behandelt werden. Das ist ganz „normal“. Denn die Schule hinkt der Realität in den Unternehmen numal hinterher. Wir wollen sicherstellen, dass die Prüflinge auf dem Arbeitsmarkt zurechtkommen. Und dazu müssen sie zeigen, dass sie übliche Methoden einsetzen können.