Sebastian Hempel bio photo

Sebastian Hempel

Software Crafter, Clean-Code-Developer, JEE Professional, Puppet-Master, OpenSource Fanboy, Alfisti

Email Twitter Google+ XING Instagram Github

Die Testgetriebene Entwicklung ist eine agile Methode zur Entwicklung von Software. Im Gegensatz zur herkömmlichen Entwicklung werden bei dieser Methode die Tests vor der eigentlich zu testenden Komponente geschrieben. Aus diesem Grund wird im Englischen neben Test Driven Development (TDD) auch von Test First Development gesprochen.

Ich möchte an dieser Stelle keine weitere Einführung in TDD geben. Hierzu gibt es neben dem Standardwerk von Kent Beck “Test Driven Development by Example” weitere – auch deutschsprachige – Literatur und zahlreiche Einführungen im Internet. Als Einstiegspunkt sei die Wikipedia-Seite zum TDD empfohlen.

Anhand der drei Schritte red – green – refactor einer Iteration beim TDD möchte ich meine Gedanken und meine Vorgehensweise beim testgetriebenen Entwickeln beschreiben.

Red

Jede Iteration beginnt mit dem Schritt Red. In diesem Schritt schreibt man einen Test.

Ich schreibe den Test aus der Sicht des Anwenders meines Codes. Dadurch kann ich mich voll auf das Verhalten des zu erstellenden Codes konzentrieren. Wie dieses Verhalten implementiert wird, ist mir zu diesem Zeitpunkt egal. Ich konzentriere mich in diesem Schritt voll uns ganz darauf ein einfaches, leicht zu nutzendes Interface zu entwerfen.

Bei der Implementierung eines DAO mache ich mir z.B. keine Gedanken, wie ich auf die Datenbank zugreife und in welcher Struktur die Informationen in der Datenbank vorliegen. Für mich kommt es in diesem Schritt einzig und allein darauf an, z.B. eine Methode getCustomer zu testen, die mir den Kunden zu einer gegebenen Kundennummer ermittelt.

Es ist darauf zu achten, dass ich mit einem Test nicht zu viel Verhalten auf einmal teste. Man spricht bei den Iterationen im TDD auch von Mikro-Iterationen. Mit jedem Test ist nur ein Aspekt der zu schaffenden Komponente zu testen.

Bezogen auf das obige Beispiel mit der DAO zur Ermittlung eines Kunden könnte ich bei der nächsten Iteration testen, wie sich die Komponente verhält, wenn ich eine Kundennummer übergebe, zu der es keinen Kunden gibt. Soll der Wert null zurückgeliefert werden, oder möchte ich die Komponente eine Exception werfen lassen?

Gerade wenn ich mit der Entwicklung einer Komponente beginne werde ich meinen Code nicht kompilieren können. Ich nehme in meinem Test Bezug auf Methoden und Attribute, die es noch nicht gibt. Dieser Umstand ist für Neulinge in der TDD sehr gewöhnungsbedürftig. Erst nachdem der gesamte Test-Code geschrieben ist, macht man sich an die Erstellung der fehlenden Methoden. Dabei implementiere ich aber nur die Rümpfe der Methoden, keinerlei Funktionalität.

Stellen sich bei der Erstellung des Test-Codes für mich weitere Fragen, so schreibe ich mir diese Fragen als weitere Testfälle auf und fahre mit der Implementierung meines Tests fort. Es ist wichtig sich nur um einen Aspekt auf einmal zu kümmern. Gedanken dürfen aber nicht verloren gehen, so wandern sie auf eine Test-Liste, um sie später in einer weiteren Iteration zu bearbeiten.

Ein Gedanken bei der Implementierung der obigen Methode eines DAO wäre z.B. was passiert, wenn die Datenbank nicht verfügbar ist. Um diesen Aspekt muss ich mich kümmern, aber nicht jetzt. So kann es passieren, dass während der Bearbeitung eines Testfalls, weitere Testfälle entstehen.

Green

Lässt sich mein Test kompilieren, starte ich den Test und werde diesen (hoffentlich) scheitern sehen.

Im zweiten Schritt der Iteration geht es darum, den Test so schnell als möglich erfolgreich zum durchlaufen zu bewegen. Es kommt dabei nicht auf guten Programmierstil oder effektiven Code an. Erlaubt ist alles, was den Test erfolgreich macht: fest Rückgabewerte, fest codierte Parameter alles ist möglich und in diesem Schritt auch erlaubt.

In diesem Schritt nehme ich davon Abstand sofort eine saubere Implementierung zu erstellen. Wo offensichtlich werde ich natürlich gleich eine Klassenvariable einführen und nicht mit einer lokalen Variable arbeiten. Bin ich mir aber unsicher, so nehme ich die einfachere Lösung und kümmere mich im nächsten Schritt oder erst in einem weiteren Test um die Ausarbeitung des Aspekts.

Ist der Test von Anfang an erfolgreich, so ist das ein Zeichen dafür, dass mein Test trivial ist, keinen neuen Aspekt meiner zu erstellenden Komponente enthält und meine Komponente evtl. einen anderen Fehler enthält.

Refactor

Der Test ist jetzt erfolgreich. Der Weg dorthin ist aber alles andere als gute Code. Im letzten Schritt einer Iteration geht es darum, den Code ins Reine zu bringen.

Wie die gesamte Entwicklung wird auch die Refakturierung in kleinen Schritten durchgeführt. Nach jeder Refakturierung wird der Test erneut gestartet. Ist der Test weiterhin erfolgreich, kann mit der nächsten Refakturierung fortgefahren werden. Schlägt der Test fehl, nimmt man die letzte Refakturierung zurück.

Kandidaten für eine Refakturierung sind z.B. Kopien von Werten sowohl im Test-Code als auch in der eigentlichen Komponente. Kent Beck spricht im allgemeinen von “Duplications”. Ich persönliche gehe aber oft so vor, dass ich mir den im zweiten Schritt erstellten Code ansehe und nach Erfahrung und Gefühl Änderungen vornehme.

Wichtig bei der Refakturierung ist, dass man nur das bestehende Verhalten umstrukturiert. Es darf auf keinen Fall neues Verhalten implementiert werden. Neues Verhalten wird nur nach dem Schreiben eines entsprechenden Test implementiert. Fügt man ein neues Verhalten durch Refakturierung hinzu, gibt es für dieses Verhalten keinen zugehörigen Test-Code. Woher soll ich dann wissen, ob das Verhalten auch richtig implementiert ist?

Man sollte sich auch dafür hüten bei der Refakturierung auf evtl. zukünftige Bedürfnisse einzugehen. Die Implementierung berücksichtigt nur die Anforderungen, die bereits festgelegt sind. Jegliche Implementierung von nicht benötigtem Verhalten verstößt gegen das YAGNI (you ain’t gonna need it) Prinzip des Extreme Programming (XP).

Iterationen

Die oben genannten Schritte red – green – refactor werden für eine zu erstellende Komponente so lange durchgeführt, bis alle Anforderungen implementiert sind.

In der Praxis wird man sich eine Testliste anlegen, auf der alle Anforderungen an die zu erstellende Komponente aufgelistet sind. Im Laufe der Iterationen werden weitere Anforderungen zur Liste hinzugefügt. Diese weiteren Anforderungen ergeben sich aus der Programmierung.

Ist die Liste abgearbeitet gilt die Komponente als implementiert. Das heißt aber nicht, dass in Zukunft nicht weitere Änderungen oder Erweiterungen an ihr durchgeführt werden. Es ist im Sinne des Extreme Programming, dass Anforderungen erst dann implementiert werden, wenn sie wirklich benötigt werden. So können Anforderungen an andere Komponenten die Erweiterung einer bestehenden Komponente bedingen.

Dokumentation

Das Ergebnis des TDD ist nicht nur ein jederzeit testbarer Code. Der Test selbst dient auch als Dokumentation der Komponente. Jeder Programmierer kann anhand des Tests das Verhalten der Komponenten erkennen.

Vorteil der Dokumentation durch einen Test ist, das notwendige Änderungen am Test-Code aufgrund der Änderungen von Interfaces immer durchgeführt werden. Ziel des TDD ist, dass nach dem Build eines Systems alle jemals geschriebenen Tests erfolgreich durchlaufen. Dadurch müssen Änderungen am Code, die einen Test betreffen dazu führen, dass auch der Test angepasst wird.

TDD als Werkzeug des Software Handwerkers

Für mich ist TDD noch nicht zur Selbstverständlichkeit geworden. Noch sehr oft möchte ich in die herkömmliche Entwicklung wechseln. Die Vorteile des TDD leuchten mir aber immer mehr ein. Gerade die Konzentration auf einen Aspekt hilft mir, wirklich guten und korrekten Code zu schreiben.

Zwar mache ich mir vor der eigentlichen Implementierung immer noch Gedanken, wie ich eine Funktionalität implementiere. Die Details lasse ich immer öfter außen vor und mache mir darüber Gedanken, wenn es für das Schreiben eines Tests / der Implementierung einer Anforderung notwendig ist. Das macht den Kopf frei für das Design der gesamten Anwendung und hilft die Details im Kontext zu betrachten.

Die Entwicklung einer Software läuft in einem Fluss. Nicht verachten dabei ist, dass es einfach Spaß macht mit TDD zu arbeiten.