Funktionsweise und Zusatznutzen von Strict TDD

Seite 3: Zyklisch und kontinuierlich

Inhaltsverzeichnis

Grundsätzlich ist der TDD-Prozess "fraktal", also unabhängig von der Granularität des Testgegenstandes (Klasse, Komponente, System). Allerdings dauert die Umsetzung eines grobgranularen Tests wesentlich länger als ein TDD-Zyklus auf Unit-Ebene. Bei einem Akzeptanztest gegen die GUI oder eine Systemschnittstelle bewegen sich Entwickler schnell eher im Stunden- als im erstrebenswerten Minutenbereich. Hier setzt Acceptance Test-Driven Development (ATDD) an, indem es den TDD-Zyklus hierarchisch schachtelt [3].

Ein Akzeptanztestzyklus klammert eine Menge von Unit-Test-Zyklen. Diese setzen die vom Akzeptanztest geforderte Funktion Stück für Stück um, bis auch er erfolgreich durchläuft. Dabei sollte der Entwickler den gröberen Test nach dem initialen Fehlschlag temporär deaktivieren oder nicht ausführen, damit das Rot des Akzeptanztests nicht das Feedback der Unit-Tests überlagert.

Zeichnet sich ab, dass selbst die Umsetzung eines einzelnen Unit-Tests länger dauern wird, sollten Entwickler die Strategie auch im Kleinen beherzigen und einen Hilfstest schreiben, der die Implementierung einer Teilfunktion zum Beispiel in einer Hilfsmethode treibt. Alternativ zur Kombination von Hilfstest und -methode können sie deren Aufruf durch eine Mock-Implementierung ersetzen und so die eigentliche Methode isoliert testen.

In "klassischen" Projekten entwerfen im Extremfall Architekten das gesamte Projekt vor der Implementierung auf dem Reißbrett ("big design upfront", BDUF). Das Problem dabei ist eine viel zu lange Feedback-Schleife: Es stellt sich erst relativ spät heraus, ob das im "Elfenbeinturm" erdachte Design in der Praxis funktioniert. Der agile Gegenentwurf dazu ist das sogenannte "emergente Design", das durch Anwendung von TDD und Refactoring kontinuierlich "wächst".

TDD und Design sind eng aneinander gekoppelt. Führt dann TDD zu einem guten Design? Nicht automatisch. TDD liefert zwar sofort Feedback aufs Design – grob gesagt: Was schwer zu testen ist, hat auch Designprobleme –, doch Entwickler brauchen für TDD gute Design-Skills. Nur so können sie erkennen, was sie am Design verbessern müssen, um es unter Test zu bekommen.

Diskussionswürdig ist, wie viel Designarbeit schon beim Schreiben des Tests geschehen soll. Beim Test-first-Ansatz sehen sich TDD-Neulinge gerade beim ersten Test mit einem vermeintlichen Henne-Ei-Problem konfrontiert. Wie sollen sie einen Test gegen eine noch nicht existierende Schnittstelle schreiben, wenn sie noch keinen Produktivcode entwickeln dürfen? Üblicherweise entsteht das Schnittstellen-Design bereits während der Testerstellung. Dazu schreibt man einen Test inklusive Beispielaufruf einer bis jetzt noch nicht existenten Methode. In einer mehr statischen Sprache wie Java unterringelt die IDE diesen Aufruf wegen eines Kompilierfehlers rot und bietet an, eine Methode mit der zum Beispiel passenden Signatur zu erzeugen. Der Test fungiert also als erster Client und setzt typische Aufrufe auf die Schnittstelle ab. Das liefert sofort Feedback bezüglich der Brauchbarkeit des Schnittstellendesigns.

Eine extreme Variante lernt kennen, wer sich mit der fortgeschrittenen Übung "TDD as if you meant it" auseinandersetzt: Darin wird das Schnittstellen-Design komplett auf die Refactoring-Phase verschoben. Dazu starten der Entwickler jegliche Implementierung initial direkt in der Testmethode und darf sie erst während der Refactoring-Phase in Methoden, Klassen und Interfaces extrahieren.

Implementierung und Refactoring sind klar getrennt: In der Implementierungsphase sollten sich Entwickler bewusst auf die einfachstmögliche Implementierung konzentrieren und jegliche Fragen zu gutem Design gezielt auf das anschließende Refactoring verschieben. Ein Refactoring [4] transformiert ein Design A in ein Design B. Dabei darf sich die bestehende Funktion nicht verändern. Eine Kultur des konsequenten Refactoring geht daher Hand in Hand mit TDD und einer hohen Testabdeckung. Denn andernfalls müssen Entwickler bei Änderungen an bestehendem Code immer fürchten, dass sie bestehende Funktionen brechen.

Die Refactoring-Phase kann allerdings einen ganzen Stapel verschiedener Refactorings umfassen. Im Sinne des schnellen Feedbacks sollte man auch hier versuchen, die Phase in kleinstmögliche "baby steps" zu zerteilen, sodass nach jedem Mikro-Refactoring die Tests wieder grün werden. Konsequent eingesetzt halten die Refactorings das Design permanent sauber. Damit verlieren die unliebsamen Makro-Refactorings mehr und mehr an Bedeutung, die sowohl riskanter als auch schwieriger einzuplanen und dem Kunden zu verkaufen sind.

XP bietet zur Orientierung beim Refactoring folgende minimale Menge von Anforderungen an gutes Design: ausdrucksstarke Namen, keine Codeduplikation sowie minimale Methoden, Klassen und Module. So entsteht durch TDD ein modulares Design aus vielen kleinen Einheiten, die sich leicht isoliert testen, flexibel wiederverwenden und gut warten lassen. Ziel ist, das Design zu jedem Zeitpunkt so einfach wie möglich zu halten, um durch Over-Engineering bedingte unnötige Komplexität zu vermeiden. Designentscheidungen werden zum spätestmöglichen Zeitpunkt mit dem bestmöglichen Wissen getroffen. Bei der Umsetzung helfen vor allem Extract-, Inline- und Rename-Refactorings. Moderne IDEs steigern durch inzwischen umfangreiche und durch Tastenkürzel effektive Refactoring-Unterstützung Sicherheit und Geschwindigkeit. Außerdem ist unbedingt der Testcode zu refaktorieren, da auch dieser sonst schnell zum Wartungsmonster mutiert.