Um Möglichkeiten, Abhängigkeiten in Tests loszuwerden, geht es in
der einhundertdreiundvierzigsten Episode des
Anwendungsentwickler-Podcasts. TL;DR Mit Test-Doubles ersetze ich
Abhängigkeiten beim automatisierten Testen objektorientierter
Software, damit du als IT-Azubi einzelne Komponenten schnell und
verlässlich prüfen kannst. Fakes bieten vereinfachte
Implementierungen; Dummies erfüllen die Rolle eines Platzhalters.
Stubs liefern vorgegebene Eingangswerte für Tests am System Under
Test, während du mit Mocks dessen Methodenaufrufe an Abhängigkeiten
prüfst. Bei Spies hängt die genaue Bedeutung vom Framework ab.
Polymorphie und Dependency Injection machen die Objekte
austauschbar, ergänzende Integrationstests prüfen das Zusammenspiel
der echten Komponenten. Kapitelmarken [00:00:01] Einstieg ins Thema
Test-Doubles [00:01:43] Grundlagen und Fokus auf objektorientierte
Software [00:02:56] Anforderungen an automatisierte Tests
[00:05:18] Unit-Tests und Integrationstests [00:06:23]
Infrastruktur als Abhängigkeit im Test [00:08:21] Zusammenarbeit
von Objekten und notwendige Abhängigkeiten [00:09:36] Beispiel:
Login-Service und Repository [00:11:34] System Under Test und
kontrollierte Testdaten [00:14:08] Test-Double als Oberbegriff für
Ersatzobjekte [00:15:39] Polymorphie als Voraussetzung [00:16:37]
Dependency Injection und testbarer Produktivcode [00:19:42]
Mocking-Frameworks erstellen Test-Doubles [00:21:20] Fakes mit
vereinfachter Implementierung [00:23:09] Dummies als Platzhalter
[00:26:46] Stubs liefern vorgegebene Eingangswerte [00:29:59] Stubs
mit Mockito konfigurieren [00:32:48] Queries, Commands und
unabhängige Tests [00:36:39] Mocks prüfen ausgehende
Methodenaufrufe [00:39:31] Beispiel: E-Mail-Versand mit einem Mock
prüfen [00:41:53] Umfangreiche Tests und vermischte Aufgaben
[00:43:14] Spies und ihre beiden Definitionen [00:46:20] Die Arten
von Test-Doubles im Überblick [00:48:21] Vorteile und Grenzen von
Test-Doubles [00:51:01] Framework-Begriffe und Wissen für die
Prüfung [00:52:13] Echte Implementierungen und ergänzende
Integrationstests [00:54:07] Verschachtelte Doubles und komplexe
Test-Setups [00:55:19] Erfahrungen aus der Praxis und Bedeutung für
IT-Azubis [00:57:50] Literatur zum Einstieg und weiterführende
Episode [00:58:39] Shownotes und Feedback 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....
Kommentare (0)
Melde dich an, um einen Kommentar zu schreiben.