Keine Zeit für Unit-Tests? Test First!

Die Implementierung von Unit-Tests gehört in der Software-Entwicklung mittlerweile nicht nur zum guten Ton, sondern zum Handwerkszeug. Insbesondere in agilen Software-Projekten sind Unit-Tests unentbehrlich, da sie eine wichtige Voraussetzung für Refactorings und somit auch für die Wartbarkeit der Software sind.

Allerdings beobachte ich immer wieder: Je näher die Deadline rückt, desto mehr werden Unit-Tests vernachlässigt. Dem kann meiner Meinung nach durch TDD entgegen gewirkt werden.

Was ist TDD?

TDD ist ein Akronym für Test Driven Development = Testgetriebene Entwicklung. Man unterscheidet zwischen testgetriebener Entwicklung mit Unit-Tests und testgetriebener Entwicklung mit System-Tests, wobei eine Kombination aus beidem vorgesehen ist. In diesem Artikel möchte ich mich auf TDD mit Unit-Tests konzentrieren.

Pionier auf diesem Gebiet ist Kent Beck, einer der Begründer von Extreme Programming. Der grundlegende, geradezu revolutionäre Ansatz von TDD ist, dass die Tests zuerst – vor den Features – implementiert werden. Man spricht daher auch von test first development. Nur dadurch, dass die Tests zuerst geschrieben werden, wird die Entwicklung wirklich testgetrieben: Die Testimplementierung übt einen wesentlichen Einfluss auf die Implementierung der Features aus.

Ein zentrales Prinzip von TDD ist der sogenannte Red/Green/Refactor-Zyklus. Er wird für jedes neue Feature (mehrfach) durchlaufen.

  • Red: Es wird zunächst keine Funktionalität implementiert, sondern nur ein Code-Gerüst erstellt, welches das Kompilieren des Unit-Tests ermöglicht (z.B. Methode mit Rückgabewert null). Der Unit-Test für das neue Feature wird implementiert und schlägt erst einmal fehl.
  • Green: Die Funktionalität wird implementiert und ggf. so lange korrigiert, bis der Test keinen Fehler mehr meldet. Der Fokus liegt hierbei vorerst auf der Funktionalität, nicht auf der Code-Qualität. Es sollte zudem nur so viel Code implementiert werden, wie für den jeweiligen Testfall nötig ist.
  • Refactor: Nachdem durch den grünen Test nachgewiesen wurde, dass die gewünschte Funktionalität implementiert wurde, lässt die Code-Qualität manchmal noch zu wünschen übrig. In diesem Fall soll jetzt der Quellcode verbessert werden und die Tests werden erneut ausgeführt.

Vorteile von Test First

Ich selbst habe erst vor etwa einem Jahr damit begonnen, die Tests zuerst zu implementieren. Je mehr ich mich an die neue Vorgehensweise gewöhne, desto weniger möchte ich “auf herkömmliche Weise” entwickeln. Der Grund hierfür liegt in den zahlreichen Vorteilen, die TDD mit sich bringt.

Unit-Tests fallen nicht unter den Tisch

Implementiert man die Unit-Tests erst nach der Funktionalität, werden sie unter Zeitdruck oft weggelassen. Und es gibt wenige Projekte, in denen man keinen Zeitdruck hat… Bei Test First entsteht erst gar keine Funktionalität ohne Tests.

Bessere Testbarkeit

Der Quellcode wird automatisch testbar implementiert. Ein großes Problem beim “Test-Danach-Ansatz” ist oft, dass die Testbarkeit des Quellcodes bei der Implementierung vernachlässigt wird. Oft verbaut man sich dadurch auch den Weg zu einer einfachen Testbarkeit.

Evolvierbare Software-Architektur

Gehen wir davon aus, dass durch Test First gut testbarer Code entsteht, so hat man einen weiteren Vorteil:  Gut testbarer Code hat in der Regel eine geringe Komplexität und wenige Abhängigkeiten zu anderen Klassen. Test-First führt in der Regel zu kleinen Klassen mit klar definierter Verantwortlichkeit, forciert also die Einhaltung des SRP-Prinzip. Im Kontrast zu monolithischen Gottklassen führen solche kleinen Klassen zu einer sauberen, evolvierbaren Software-Architektur.

Vermeiden von AssertionFreeTesting

Die Gefahr beim “Test-Danach-Ansatz” besteht darin, dass Grenzbedingungen und Ausnahmen vergessen werden. Es kann sogar dazu führen, dass Tests überhaupt keine Fehler abfangen und somit nutzlos sind. Martin Fowler nennt dies zutreffend AssertionFreeTesting. Test First beugt diesem Problem durch den Red/Green/Refactor-Zyklus vor.

Keine Angst vor Refactorings

Test First stärkt das Vertrauen der Entwickler in den Quellcode und nimmt somit die Angst vor Refactorings. Dies ist enorm wichtig, da agile Softwareentwicklung nur dann langfristig möglich ist, wenn regelmäßige Refactorings durchgeführt werden.

Stabiler, korrekter Code

Außerdem habe ich beobachtet, dass ich mir eher Gedanken über Grenzbedingungen und Ausnahmen mache, wenn ich die Tests zuerst schreibe. Da ich die Tests für verschiedene Bedingungen einzeln schreibe und nach jeder Änderung wieder ausführe, besteht auch kaum die Gefahr, dass ich durch die Behandlung einer zusätzlichen Bedingung einen (unentdeckten) Fehler einbaue.

Vorbehalte gegenüber Test First

Wenn Test First so viele Vorteile bringt, warum wird diese Vorgehensweise dann nicht von allen Teams praktiziert? Bei den Entwicklern trifft man immer wieder auf die gleichen Vorbehalte, wenn es um die testgetriebene Umsetzung geht. Einige dieser Vorbehalte möchte ich hier entkräften.

Unit-Tests benötigen zu viel Quellcode?

In der Regel wird man in etwa genauso viel Quellcode für Unit-Tests wie für die Funktionalität schreiben. Robert C. Martin erreicht hiermit für sein Testframework Fitnesse etwa 90% Testabdeckung.

Allerdings kann man den durch die Unit-Tests zusätzlich entstehenden Code auch positiv betrachten. Robert C. Martin vergleicht in seinem Artikel The Land that Scrum Forgot TDD mit der doppelten Buchführung aus dem Rechnungswesen: Tests sind kein Ballast, sondern ein zusätzliches Sicherheitsnetz. Außerdem sollten die Unit-Tests eine zusätzliche Funktion als Low-Level-Dokumentation darstellen. Voraussetzung hierfür ist natürlich, dass die Tests genauso sauber – und somit verständlich – wie der restliche Quellcode implementiert sind.

Wenn mehr Code für Unit-Tests als für die eigentliche Funktionalität nötig ist, so ist das meistens ein Signal dafür, dass der Code schlecht testbar ist oder dass der Testcode nicht nach dem DRY-Prinzip implementiert wird.

Unit-Tests brauchen zu viel Zeit?

Es ist richtig, dass die Implementierung etwas länger dauert, wenn man zusätzlich zur eigentlichen Funktionalität auch noch Unit-Tests schreibt. Allerdings zahlt sich der etwas höhere initiale Implementierungsaufwand langfristig durch niedrigere Wartungskosten aus.

Sogar der  initiale Implementierungsaufwand kann beim TDD-Ansatz niedriger sein, wenn man die Zeit fürs manuelle Testen inklusive Debugging miteinbezieht. Wenn ich die Tests zuerst schreibe, werde ich durch fehlschlagende Tests auf Fehler aufmerksam und benötige deutlich weniger Zeit für manuelles Testen.

Es gibt natürlich Fälle, in denen man aus Zeitgründen auf Tests verzichten kann. Für einen Marketing-Prototyp oder eine Proof-Of-Concept-Implementierung wird man normalerweise keine Unit-Tests schreiben. Auch um die Deadline eines kritischen Projekts halten zu können, kann man in Betracht ziehen, kurzfristig darauf zu verzichten. Man sollte sich allerdings bewusst sein, dass man hierdurch technische Schulden aufnimmt, die möglichst bald abbezahlt werden sollten.

Unit-Tests sind schwierig zu implementieren?

Gute Unit-Tests zu schreiben ist schwierig. Insbesondere dann, wenn man jahrelang auf “herkömmliche Weise” programmiert hat. Der Test-First-Ansatz erfordert ein Umdenken, das nicht von heute auf morgen möglich ist.

Leider wurde dem Thema TDD und Unit Testing bisher an den Hochschulen recht wenig Bedeutung beigemessen. Ich habe vor vier Jahren mein Studium abgeschlossen und hatte damals keine einzige Vorlesung, in der ich einen Unit-Test schreiben musste. Man kann also von Absolventen nicht unbedingt erwarten, dass sie von heute auf morgen testgetrieben entwickeln können. Hier sind erfahrene Entwickler als Mentoren gefragt.

Je nach Art der Software (Webanwendung, Desktopanwendung, Webservice) stellen sich verschiedene Herausforderungen beim Unit-Testing. Eine dieser Herausforderungen sind Abhängigkeiten zu externen Bibliotheken oder Frameworks, die ohne Fokus auf Testbarkeit entworfen wurden. Der Einsatz eines guten Mock-Frameworks ist da unerlässlich. Mein persönlicher Favorit im Java-Umfeld ist Mockito, allerdings gibt es viele gute Alternativen. Um den Einarbeitungsaufwand möglichst gering zu halten, ist es wichtig, dass man sich im Team für ein Mock-Framework entscheidet.

Die größte Hürde für TDD stellen Legacy-Anwendungen dar, für welche noch keine Unit-Tests existieren. Hier ist eine spezielle Herangehensweise sinnvoll, die Michael Feathers in seinem Buch Working Effectively with Legacy Code beschreibt.

Wie fange ich an?

Probieren geht über Studieren! Allerdings gibt es verschiedene Möglichkeiten, TDD auszuprobieren. In einem bereits begonnenen Projekt TDD nachträglich einzuführen, kann ziemlich schwierig sein und es besteht die Gefahr, dass man aufgrund der Schwierigkeiten recht schnell aufgibt.

Ich persönlich löse gerne Code-Katas und habe damit begonnen, diese testgetrieben zu entwickeln. Man hat so die Möglichkeit, sich gezielt auf den testgetriebenen Ansatz zu konzentrieren und diesen zu verinnerlichen, ohne sich um – oft nicht wirklich spannende, aber doch nicht triviale – Business-Probleme kümmern zu müssen. Ganz nebenbei machen Code-Katas auch noch Spaß. Mit den so gewonnenen Erfolgserlebnissen ist es mir leichter gefallen, auch Produktiv-Code testgetrieben zu entwickeln. Allerdings muss ich zugeben, dass ich manchmal immer noch versehentlich die Funktionalität zuerst implementiere.

Fazit

Mir macht Unit-Testing deutlich mehr Spaß, wenn ich die Tests vor der eigentlichen Funktionalität schreibe. Die Implementierung  eines Testfalls ist für mich die Pflicht, auf welche die Kür – die Implementierung der eigentlichen Funktionalität – folgt. Allen, für die Test First nur graue Theorie ist, werde ich weiterhin empfehlen: Probiert es aus!

Als Übung werde ich in einem folgenden Artikel eine Kata zum testgetriebenen Entwickeln vorstellen.

Keine Zeit für Unit-Tests? Test First!, 4.1 out of 5 based on 11 ratings

Tags: , , , ,

Kommentar schreiben:

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind markiert *


8 + = zwölf

Du kannst folgende HTML-Tags benutzen: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>