Test Driven Development (Teil 1 von 3)
Test Driven Development auf das Wesentliche runtergebrochen ist folgender dreiteiliger Kreislauf, der wiederholt und möglichst kleinschrittig durchlaufen wird:
- 1. Roter Pfad
- Einen Testfall schreiben, der initial fehlschlägt (wäre der Test von Anfang an grün, wüsste man nicht, ob er das richtige testet oder überhaupt aufgerufen wird, so ein
@Test
vergisst man ja auch gerne mal). - 2. Grüner Pfad
- Auf dem schnellsten Weg und ohne Schnörkel, Umwege oder Verschönerungen den Testfall grün kriegen.
- 3. Refactoring
- Hübsch machen, Refactoren usw. – allerdings immer nur dann, wenn alles grün ist. Sobald beim Refactoren in Testfall rot wird, weiß man, dass man etwas kaputt gemacht hat.
Da man über 1. und 2. nur Code schreibt, für den man einen Test hat, ist anschließend tatsächlich automatisch immer alles (unit-)getestet.
Roter Test
Ein roter Test ist ein wunderbares Lesezeichen: Wenn man seine Arbeit kurz unterbrechen muss (Telefon, Mittagessen, Meeting), dann lässt man zum Weitermachen einmal die Tests laufen und sieht, woran man das letzte Mal gerade gearbeitet hat. Man kann auch absichtlich vor dem Feierabend oder dem Wochenende einen neuen roten Test schreiben – nur sollte man den natürlich dann nicht einchecken, sonst geht der Build kaputt.
Meine Erfahrung: das klappt wirklich. Plötzlich hereinrauschende Kollegen mit Fragen zu ganz anderen Themen bringen mich nicht mehr so aus dem Tritt wie früher. Einerseits habe ich einen roten Test, um zu gucken, wo ich gerade war und andererseits programmiere ich testgetrieben in kleineren Häppchen, wodurch beim Kontextwechsel nur viel kleinere gedankliche Kartenhäuser zusammenstürzen.
Schrittgröße
Eine relevanter Punkt beim TDD ist die Schrittgröße: Es ist sinnvoll, den oben dargestellten Zyklus möglichst oft und kleinschrittig zu durchlaufen. Wenn dabei ein Test rot wird, weiß man sofort, woran es lag: An der Sache, die man zuletzt geändert hat. (Mit IntelliJ kann man dann z.B. seine Änderungen direkt bis zum letzten grünen Stand rückgängig machen, bei Eclipse habe ich dafür noch kein passendes Plugin gefunden.)
Mit zunehmender Erfahrung wird man größere Schritte machen (wollen). Das ist ok.
Irgendwann wird dann unerwartet mal ein Test rot und es gibt diesen „Hä?“-Effekt: Kann ja gar nicht sein, wo soll das denn jetzt herkommen, so viel habe ich doch jetzt gar nicht geändert und schon gar nichts, was jetzt mit dem Test zu tun hat. Dann steigt man in die Fehlersuche ein und da will man ja eigentlich gar nicht sein. Klarer Fall von zu großen Schritten und zu viel auf einmal. Das ist auch ok. Danach macht man erstmal wieder kleinere Schritte.
Das pendelt sich ein ;-)
Architekturauswirkungen
Ich würde ja testen, aber das geht bei meinem Code gar nicht!
Wenn man feststellt, dass Code schlecht testbar ist, ist das meist ein Hinweis, sich über seine Komponentenbildung Gedanken zu machen. Vermutlich baut man gerade einen Monolithen, den man besser in mehrere Teile zerlegt, die man dann einzeln testen kann. Wenn es gut läuft, werden aus ellenlangen Mockingkaskaden plötzlich mehrere simple Testfälle für verschiedene Einzelkomponenten. So nimmt das Test-Schreiben durchaus Einfluss auf bereits vorhandene Anwendungs-Architektur.
Aber auch auf der „grünen Wiese“ kann TDD in die Architektur eingreifen: Wenn man Test für Test Stück für Stück entwickelt, wird sich z.B. eine Klassenhierarchie während des Entwickelns dynamisch ganz von selbst entwickeln, wenn man die ersten Redundanzen und Ähnlichkeiten findet. Ohne vorher zwei Tage nachzudenken, was man denn vielleicht später mal alles brauchen könnte – es wird nur das umgesetzt, was man aktuell braucht.
Größter Vorteil hierbei: Durch die Testabsicherung und die potenziell kleineren Komponenten hat man es später einfacher, wenn man aufgrund neuer Gegebenheiten oder Anforderungen irgendetwas an seiner Architektur ändern muss. Sie lebt und ist wandelbar statt in Stein gemeißelt.
Trenne Tests vom Rest
Wenn man testgetrieben mit kleinen Detail-Funktionalitäten anfängt und daraus das große Ganze entwickelt (Inside-Out), dann kann man Dinge, die in eine einzelne Methode passen, anfangs erstmal ruhig direkt innerhalb des Tests kodieren. Später kann man die Methode im Rahmen des Refactorings immer noch in eine geeignete (ggf. neue) Klasse verschieben.
Auch wenn man sich von den Schnittstellen zu einer Detailimplementierung vorantastet (Outside-In), kann man das innerhalb des Tests tun (z.B. über eine innere Klasse) und später refactoren.
Wenn man ohne Feature-Branches oder ähnliches entwickelt, aber eine saubere Trennung zwischen Tests und Produktivcode hat (z.B. src/main/
und src/test/
in Java oder lib/
und t/
in Perl), kann man auch unfertigen Code einchecken, solange er noch bei den Tests liegt. So kann man im Produktivcode nichts durcheinanderbringen. Praktisch, wenn man ein größeres Feature implementiert, das man nur komplett fertig auf die Welt loslassen will.
Ausblick
Im nächsten Teil gibt es dann mal ein illustriertes Beispiel (ja, auch hier im Blog betreibe ich zunehmend Komponentenbildung, weil ein ultralanger Monsterartikel nun architektonisch auch nicht so toll ist).
Mitch’s Manga Blog am : Test Driven Development (Teil 2 von 3)
Mitch’s Manga Blog am : Agile Software Engineering
Mitch’s Manga Blog am : Test Driven Development (Teil 3 von 3)
Netz - Rettung - Recht am : Wellenreiten 10/2016
Mitch’s Manga Blog am : omnomnom - Nomitoring für Zwischendurch (Teil 3/3)