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)
Melde dich an, um einen Kommentar zu schreiben.