Wir schreiben Software. Wir entwickeln Services und verteilen diese in Umgebungen. Wir benutzen sie. Alles funktioniert super. Warum sollen wir also testen? Warum sollen wir ausgerechnet automatisiert testen?
Der folgende Blog-Beitrag setzt sich mit verschiedenen Motivationen auseinander, warum automatisiertes Testen sinnvoll ist. Automatisierte Tests unterstützen viele Ziele, lösen aber – wenig überraschend – nicht jedes Problem.
Nicht in jedem Umfeld sind die Vorteile und Ziele automatisierten Testens gleichermaßen gültig.
Für Manager: Zeit und Kosten sparen
Mit automatisierten Tests lassen sich Zeit und Kosten sparen – zumindest mittel- und langfristig.
Kurzfristig kostet Test-Automatisierung mehr: Ich muss die Tests schließlich erst schreiben und automatisieren. Aber mit jeder Ausführung zahlen automatisierte Tests einen Teil der Kosten zurück (Abbildung unten: Kosten vs. Zeit, rote durchgezogene Linie). Wieder und wieder und wieder.
Manuelle Tests kosten kurzfristig weniger – allerdings nur sehr kurzfristig (blaue durchgezogene Linie). Ein automatisierter Test lohnt sich vermutlich ab der dritten Ausführung. Als Entwickler habe ich einen automatisierten Test nach zehn Minuten Entwicklung bereits mehr als dreimal ausgeführt.
Software neigt zur Veränderung: Änderungen am Code gehen mit Änderungen an Tests oder im Idealfall mit neuen Tests einher. Bei manuellem Testvorgehen mit Veränderungen steigen die Kosten früher an als ohne Veränderung (blaue gestrichelte Linie). Auch die Veränderung automatisierter Tests oder neue automatisierte Tests kosten mehr (rote gestrichelte Linie). An der Erfahrung, dass automatisierte Tests langfristig kostengünstiger sind als manuelle Tests, ändert das aber nichts.
Noch preiswerter ist es, nicht zu testen. Allerdings nur so lange ich noch kein Stück Code geschrieben und ausgeführt habe. Nein – nicht zu testen ist selbst unter Zeit- und Kosten-Gesichtspunkten keine Option.
Für Ungeduldige: Frühes Feedback
Automatisierte Tests werden vom Entwickler lokal nach jeder Code-Änderung oder in der Build Pipeline automatisch ausgeführt. Schlägt ein Test fehl, bekomme ich sofort Feedback über den Fehlschlag – fail fast.
Schlägt der Test in der IDE fehl, muss ich die Ursache für den Fehlschlag finden und beheben. Danach kann ich weiterentwickeln oder in die Versionsverwaltung einchecken. Tue ich das nicht, fällt es mir im Nachgang wahrscheinlich immer schwerer, die Fehlerursache zu identifizieren. Der Fail-Fast-Aspekt geht verloren.
Für Prozessbewusste: Automatisierung wiederkehrender Tätigkeiten
Ein Deployment, ein Build, ein lokaler Testlauf – alle diese Prozessschritte sind darauf angewiesen, bei ihrer Durchführung minimale Transaktionskosten auszulösen. Wir führen die Prozessschritte so oft wie nötig durch, ohne Fragen nach dem Aufwand zu stellen.
Automatisierte Tests zahlen in diese Minimierung der Transaktionskosten ein. Das Zeitalter von manuellem Build & Deployment mit wochenlangen manuellen Testphasen ist vorbei.
Für Puristen: Einfaches Design
Photo by Jozsef Hocza on Unsplash
Testgetriebenes Design – also Code so zu schreiben, dass er einen Test erfüllt – führt potenziell zu einfacherem Design. Der Test gibt dem Code eine Richtung vor. Boilerplate Code – also Code der nicht zur Lösung des eigentlichen Problems beiträgt – vermeiden oder minimieren wir.
Für Vorsichtige: Validierung von Refactoring
Anforderungen und Rahmenbedingungen einer Software verändern sich über die Zeit und bedingen die Änderung von Code. Wir stellen bei Code-Änderungen sicher, dass von geänderten Anforderungen nicht betroffene Funktionalität erhalten bleibt.
Automatisierte Tests helfen bei der Validierung von Refactorings. Sind keine, zu wenige oder die falschen Tests vorhanden, ist es schwer zu validieren, welche Auswirkungen eine Code-Änderung hat.
Aber Vorsicht: Automatisierte Tests sind ein Indikator für die Funktion von Code – keine Garantie. Diese Garantie gewähren nur vollständige Tests und diese sind selbst bei einfachen Anforderungen nur mit unvertretbar hohem Aufwand leistbar. Mit unvertretbar meine ich nicht nur unvertretbar, sondern unrealistisch. Wirklich. Ein kleines Beispiel gefällig?
Wir haben eine Funktion geschrieben, um zwei Integer-Zahlen zu multiplizieren, und wollen diese vollständig testen.
Integer-Zahlen haben einen Wertebereich von -2.147.483.648 bis 2.147.483.647.
Das bedeutet, eine Integer-Zahl kann 4.294.967.296 verschiedene Werte annehmen. Wir haben aber nicht eine Integer-Zahl, sondern gleich zwei. Jeder Wert der einen Zahl kann mit jedem Wert der anderen Zahl multipliziert werden. Das ergibt 4.294.967.296 Werte mal 4.294.967.296 Werte.
Anders ausgedrückt: Es sind 18.446.744.073.709.550.000 oder 18,4 Trillionen verschiedene Multiplikationen möglich. 18,4 Trillionen Testdurchläufe sind notwendig, um die Multiplikation zweier Integer-Zahlen vollständig zu testen.
Das. Ist. Nicht. Machbar.
Üblicherweise besteht die Funktionalität moderner IT-Systeme aus mehr als der Multiplikation zweier Integer-Zahlen. Ihre Funktionalität ist deutlich komplexer. Hier vollständig zu testen ist noch weniger realistisch.
Für Code-Versteher: Dokumentation von Funktionalität
Automatisierte Tests[1] gehören zur Dokumentation der Funktionen, die sie testen. Will ich verstehen, wie ein Stück Code funktioniert, lese ich den Code. Ich kann mir aber nicht sicher sein, seine Funktion richtig zu verstehen. Hier helfen Beispiele, die den Code in Aktion zeigen und sein Ergebnis messen.
Für Nachhaltige: Fehler nachvollziehen
Tritt nach Abschluss der Entwicklung an einer Funktionalität ein Fehler auf, versuche ich, den Fehler nachzuvollziehen. Eventuell gelingt mir das durch Betrachten des Codes. Ich kann den Code anpassen und hoffen, den aufgetretenen Fehler beseitigt zu haben.
Oder ich versuche, den Fehler mit Hilfe eines automatisierten Tests nachzustellen. Gelingt mir dies, kann ich meinen Code jetzt so anpassen, dass der Test, der den Fehler erzeugen kann, fehlschlägt. Schlägt der Test fehl, habe ich den Fehler mit einiger Sicherheit beseitigt. Ich will aber sicherstellen, dass der Fehler – beispielsweise durch eine andere Code-Änderung – nicht wieder auftritt.
Hierfür negiere[2] ich das Ergebnis des fehlgeschlagenen Tests. Der Test ist jetzt erfolgreich. Schlägt dieser Test jemals wieder fehl, weiß ich, dass ich einen bekannten Fehler wieder eingebaut habe. Ich kann meine Änderung, die zum Fehlschlag führte, korrigieren.
Für Wahrsager: Verhalten von Code vorhersagen
Automatisierte Tests helfen uns, das Verhalten einer Funktion beim Eintreten bestimmter Bedingungen vorherzusagen.
Hierfür werden Testdaten in Äquivalenzklassen unterteilt, mit deren Hilfe „gleichartigen“ Daten gleichartiges Verhalten unterstellt wird. Entdecke ich durch einen Fehler eine neue Äquivalenzklasse, ergänze ich einen entsprechenden Test. Und verbessere die Vorhersagbarkeit des Verhaltens meiner Funktion.
Für Erben: Weiterentwicklung erleichtern
Wenn Entwickler die Wartung oder Weiterentwicklung von bestehendem Code übernehmen, sind gute automatisierte Tests eine wesentliche Voraussetzung ihnen den Einstieg zu erleichtern. Eine weitere Voraussetzung ist gutes Design.
Aus guten Tests lässt sich das erwartete Verhalten des bestehenden Codes „lernen“. Bricht ein Test nach einer Code-Änderung, bekommt das Team schnelles Feedback. Die Lernkurve der Entwickler steigt.
Für Sicherheitsbewusste: Verhalten kritischer Funktionen nachweisen
Automatisierte Tests für den Code einer kritischen Funktionalität kann deren fachliche Korrektheit belegen. Sie kann nicht die Fehlerfreiheit der Funktionalität garantieren, sondern lediglich, dass sich die Funktionalität verhält wie von deren Entwicklern vorgesehen.
Für Sprengstoffexperten: Auswirkungen einer Änderung minimieren
Die Änderung einer häufig verwendeten Funktionalität kann gravierende Auswirkungen auf mein gesamtes System haben. Entsprechend groß ist der Explosionsradius der Änderung. Durch angemessene, automatisierte Tests kann ich den Explosionsradius von Änderungen frühzeitig feststellen. Ich kann direkt reagieren.
Für Verlegene: Vermeidung trivialer Fehler
Die wiederholte Ausführung von Code führt dazu, dass triviale Programmierfehler nicht erst den Weg in die Produktion finden. Eine Null-Pointer-Exception gleich zu Beginn der Verarbeitung? Entdecke ich vor meinen Benutzern. Eine Array-Index-Out-Of-Bounds-Exception, weil ich falsch auf einen Index zugreife? Gefunden, bevor ich den Code überhaupt eingecheckt habe.
Für Zielbewusste: Fehler früher und schneller finden
Code Coverage ist ein wertvolles Hilfsmittel für die Identifikation eines Fehlers. Kann ich einen Fehler mit einem automatisierten Test nachvollziehen, kann ich die Fehlerquelle mit Code Coverage isolieren.
Ob die Fehlerbehebung genauso schnell geht, ist eine andere Sache.
Für Spürnasen: Debuggen erleichtern
Ich kann den Fehler mit einem automatisierten Test nachstellen? Perfekt! Ich starte den Test im Debug Mode und steppe durch den Code, bis ich nachvollziehen kann, wo der Fehler auftritt.
Das funktioniert meist nur bei Unit Tests. In einem Integrationstest kann ich oft nur schwer debuggen.
Für Effektive: Zeit effektiv nutzen
Automatisierung spart Zeit und Aufwand bei der Wiederholung von Tests. Die gewonnene Zeit kann ich für die spannenden Dinge des Entwickler- oder Testerlebens verwenden. Beispielsweise mit dem Schreiben von Code oder weiteren automatisierten Tests.
Für Eilige: Kürzere Entwicklungszeit
Muss ich meine verteilte Anwendung für den automatisierten Test komplett mit all ihren Bestandteilen starten, kostet das Zeit. Viel Zeit, wenn ich das häufig tun muss. Muss ich nur einen Teil starten, um automatisiert zu testen, erhalte ich früher Feedback. Und kann früher auf Fehler reagieren. Ich bin insgesamt schneller, wenn ich oft Feedback durch Tests erhalte.
Für Statiker: Stabilität der Software erhöhen
Eine Software, die durch viele gute Tests abgedeckt ist, ist potenziell stabiler als ein manuell oder gar nicht getestetes System. Je häufiger ein Zweig mit verschiedenen Parametern durchlaufen wird, desto höher ist die Wahrscheinlichkeit, dass er in Produktion stabil bleibt. Eine Garantie gibt dies aber nicht.
Wer Annahmen darüber trifft, welche Funktionalität durchlaufen wird, soll dies testen und in die Stabilität der Funktionalität einzahlen.
Für Vordenker: Modularisierung unterstützen
Ein System, das modular aufgebaut ist, muss ich zusätzlich zum Unit-Test-Level auf diesem Level – dem Modul-Level[3] – testen. Die Granularität dieser Modul-Tests ist generell gröber als bei Unit-Tests. Werden Modul-Tests von Anfang an geschrieben und benutzt, unterstützt das meist automatisch den Modul-Charakter der betroffenen Funktionalität. Die Chance wiederverwendbare Funktionalität zu schaffen steigt.
Für Selbstständige: Unabhängiger Code
Code, der isoliert ist und wenige Abhängigkeiten besitzt, neigt dazu, besser automatisiert testbar zu sein. Wenn unabhängiger Code erklärtes Ziel ist, helfen automatisierte Tests. Viele Tests. Auf dem Level[4], auf dem ich die Unabhängigkeit anstrebe.
Für Wahrheitsbewusste: Aktuelles Verhalten dokumentieren
Tests lügen nicht – zumindest bei der Testdurchführung. Denn dann zeigen sie exakt das Verhalten der aktuellen Implementierung einer Funktion.
Führe ich einen Test nicht aus, habe ich keine Information darüber, ob der Test zu meiner aktuellen Implementierung passt. Deshalb führe ich automatisierte Tests aus, sobald es sinnvoll ist. Und das kann am Tag durchaus 200-mal der Fall sein, denn die Wahrheit kann sich heimlich verändert haben.
Für multiple Persönlichkeiten: Auf verschiedenen Zielplattformen testen
Mein Service läuft auf einem Mobiltelefon oder auf einem Großrechner? Die Verwendung darf keine Auswirkung auf die Lauffähigkeit meiner automatisierten Tests haben. Im Idealfall laufen diese unverändert in jeder Umgebung. Manchmal muss ich dafür etwas tun, beispielsweise ein Test-Framework auf einer neuen Zielplattform installieren. Und für diese Plattform konfigurative Anpassungen vornehmen. Dann sind meine Tests doch bitte auch dort lauffähig.
Für Träumer: Nicht erfüllbare Ziele für automatisiertes Testen
Folgende Ziele lösen durch automatisierte Tests nicht oder nicht vollständig:
- Unbekannte Fehler finden
- Schlechtes Design beheben
- Kostenlose Tests
- Codequalität verbessern
- Ressourcen-Probleme lösen
- Unterstützung bei Gray Failure
- Fehlerfreiheit
- Vollständiges Testen
Auf all diese Punkte gehen wir in einem späteren Blog Post im Detail ein.
[1] Dies gilt auch für manuelle Tests, wobei diese dazu neigen zu veralten.
[2] Die Methode wird Positiv-/Negativ-Test genannt.
[3] Ein Modul kann ein Service, ein Subsystem oder eine Komponente oder was auch immer sein. Entsprechend ist ein Modultest ein Test auf Service-, Subsystem- oder Komponenten-Level.
[4] Klasse, Schnittstelle, Funktion, Subsystem, …