Tests that Empower

geschrieben von Christian Klinger, Lesezeit 2 Minuten
Nicht das spannendste Thema, aber unglaublich wichtig: Automatisierte Tests in der Software-Entwicklung. Wer gute Software zuverlässig liefern möchte, kommt nicht darum, gute Tests zu schreiben. Denn (gute) Tests erhöhen nicht nur die Qualität der Software sondern ermöglichen auch erst die konsistente Weiterentwicklung über längere Zeiträume hinweg. Oder um es mit den Worten von Michael Feathers zu sagen: "untested code is legacy code".

Dafür ist es wichtig, gute Tests zu schreiben. Denn genauso wie gute Tests die Qualität erhöhen und die Entwicklungsgeschwindigkeit erhöhen, können schlechte Tests das Gegenteil bewirken. Aber was macht Tests gut oder schlecht?

Testing ist ein großes Feld und es lohnt sich einige Annahmen in Frage zu stellen und aktiv darüber nachzudenken, welche Testingstrategie für eure System am Besten passt. In diesem Blogpost möchte ich eine leicht abweichende Form der Testpyramide, nennen wir es mal 'Testraute', vorstellen.

Als grobe Richtlinie hat sich in den letzten 20 Jahre die Testpyramide entwickelt. Diese Pyramide beschreibt den Aufbau der Testsuite unterteilt in Unit-Tests, Service-Tests und UI-Tests. Über die genauen Definitionen dieser Begriffe kann man sich streiten, aber die Grundidee ist, dass man viele fein-granulare Tests hat und nur wenige grob-granulare Tests.

Begründet wird die Form der Testpyramide oft damit, dass Unit-Tests besser dabei helfen, den Fehler schnell zu lokalisieren. Das ist einleuchtend, denn wenn ein SortingAlgorithmTest fehlschlägt, ist klar, dass es einen Fehler in der Klasse SortingAlgorithm gibt. Allerdings lässt sich heutzutage, wenn man eine Versionsverwaltung hat und seine Commits kleinteilig strukturiert (ja, das sollte man!), der Fehler sowieso schnell lokalisieren. Waren die Tests gerade noch grün, so muss der Fehler offensichtlich durch die gerade vorgenommen Änderungen entstanden sein.

Ein anderes Argument für eine breite Unit-Test-Basis ist schnelles Feedback. Denn schnelles Feedback ist wichtig, um wirklich testgetrieben zu entwickeln, also um einen Test schreiben, eine kleine Änderung machen, Test ausführen, nächste Änderung etc. ohne dabei durch die Ausführung der Tests ausgebremst zu werden.

Unit-Tests haben allerdings einen gravierenden Nachteil: Sie sind sehr arbeitsintensiv bei Refactorings. Wer kennt es nicht: Den Produktivcode hätte man in 1-2 Stunden umgebaut, für die Tests braucht man aber 1-2 Tage.

Service-Tests dagegen werden als langsam und aufwendig eingestuft. Aber muss das wirklich so sein? Wenn es möglich ist, Service-Tests schnell auszuführen, dann haben Unit-Tests, im Vergleich zu Service-Tests, eigentlich nur Nachteile.

  • Pro Test wird viel weniger Code abgedeckt. Das heißt mit der gleichen Anzahl an Service-Tests kann ich mehr Zeilen Code abdecken und habe eine höhere Chance, Fehler zu finden und zukünftige Regressionen zu vermeiden.
  • Service-Tests beschreiben besser den Use-Case, sie dienen damit auch als eine Art Dokumentation.
  • Dadurch dass sie nur das Verhalten nach außen beschreiben, müssen sie bei Refactorings nicht angepasst werden. Ganz im Gegenteil: Sie beschleunigen Refactorings ungemein, weil sie quasi als Leitplanken dienen und sicherstellen, dass der Code nach dem Refactoring noch das Gleiche tut als davor.

Das heißt meine ideale Testpyramide ist eigentlich eher ein Raute. Oben haben wir einige wenige UI-Tests für kritische Stellen. Dann haben wir einen dicken Bauch an Service-Tests (auch szenario-basierte Tests) genannt, die das Verhalten unserer Anwendung nach außen testen. Unten haben wir dann noch einige Unit-Tests, die Hilfsmethoden und ähnliche Funktionalität testen, die man keinem bestimmten Szenario zuordnen kann.

Mit dieser Testraute bekommt man eine Testsuite, die das Team dabei unterstützt schnell neue Features zu entwickeln und dabei weiterhin sichere Refactorings ermöglicht ohne dabei die Entwicklungsgeschwindigkeit zu reduzieren.

Was meint ihr? Wir freuen uns auf eure Erfahrungen.