TDD in der agilen Entwicklung

In vielen agilen Projekten wird heutzutage die Testgetriebene Entwicklung (TDD) mit ihrem Ansatz „Test First“ verwendet. Auf diese Weise soll die Qualität der erzeugten Software derart verbessert werden, dass auf langwierige Testverfahren im Anschluss an die Entwicklungsphase verzichtet werden kann. Dies geht von der Prämisse aus, dass die während der Entwicklung erstellten Tests sowohl den Kriterien einer ggf. bereits wegrationalisierten Testabteilung als auch denen des Auftraggebers genügen. Häufig wird diese Annahme von Personen getroffen, die wenig Erfahrung mit agilen Prozessen haben und davon ausgehen, dass diese Methodik Einsparungen in den Produktionskosten und Verbesserungen in der Qualität bringt. Leider zeigt die Erfahrung, dass dieser Ansatz so nicht funktioniert. Daher möchte ich in diesem Artikel einige Argumente gegen diesen naiven Einsatz von TDD aufzeigen, um so einige Probleme mit dem Einsatz dieser eigentlich sinnvollen Technik aufzuzeigen.

TDD – Was war das noch genau?

In seinem Buch „Test Driven Development: By Example“ erläutert Kent Beck das Prinzip, wie TDD eingesetzt werden kann. Der Entwickler überlegt sich, welche Aufgaben seine zu entwickelnde Klasse übernehmen soll, schreibt sich diese Aufgaben auf eine Liste und arbeitet diese eine nach der anderen ab. Er verwendet dabei stets das gleiche Vorgehen:

  1. Schreiben eines Tests.
    Der Entwickler schreibt einen Unit-Test, in dem die zu implementierende Aufgabe mit (noch nicht erstellten) Methoden realisiert wird. Es wird quasi eine Blaupause für die zu erstellenden Methoden und ihre Verwendung geschaffen.
  2. Implementierung der Methoden
    Anschließend werden die in dem Test verwendeten Methoden mit Inhalt gefüllt. Dabei geht es nicht um eine elegante, sondern lediglich um die einfachste aller möglichen Lösungen. Ziel ist es, dass der Test erfolgreich läuft.
  3. Refaktorisierung der Lösung
    Anschließend wird die erstellte Implementierung entsprechend der Prinzipien von Clean Code verbessert, ohne dass der Test wieder fehlschlägt.

Da die Tests häufig mit einem Testframework wie xUnit erstellt werden und dort fehlgeschlagene Tests in Rot und erfolgreiche Tests in Grün dargestellt werden, spricht man hier gerne von der Testampel bzw. von dem Rot-Grün-Grün-Zyklus. Evtl. kann man in der Methodik noch einen weiteren Schritt aufnehmen, in dem der erzeugte Quellcode in ein Code-Versionierungssystem aufgenommen wird.

Probleme beim Arbeiten mit TDD

Die von Beck beschriebene Vorgehensweise liest sich sehr einfach, hat aber den Nachteil, dass sie lediglich auf der Ebene der Komponenten (Units) ansetzt, für das Beck und andere ihr bekanntes Unit-Test-Framework entwickelt haben (s. zum Beispiel JUnit).  In seinem Artikel „Why Most Unit Testing Is Waste“ beschreibt James O. Coplien, dass sich Unit-Testing in der imperativen Programmierung entwickelt hat, in der die zu realisierende Funktionalität in viele hierarchisch organisierte Funktionen und Prozeduren heruntergebrochen wurde, welche sich mit dieser Testmethodik gut testen ließen. Dann hat sich das Programmierparadigma hin zur objektorientierten Entwicklung verändert, wo Anwendungen mit Hilfe von Klassen realisiert werden, die eine Vermischung aus Daten und Funktionalität darstellen. Durch Klassenhierarchie, dynamische Instanzen und auch Techniken wie zum Beispiel Dependency Injection ist es schwierig, den Ablauf eines Programms nur durch Lesen des Quellcodes zu verstehen. Um schnelles Feedback und möglichst wenig Fehler in einer Anwendung zu bekommen, sind Unit-Tests auch in diesem Umfeld wieder modern geworden.

Problematisch daran ist, dass sich die eigentlichen Funktionseinheiten vielfach in viele, einfache Methoden aufteilen, so dass die in der imperativen Programmierung entwickelte Technik, einzelne Funktionen bzw. in diesem Fall Methoden zu testen so nicht sinnvoll eingesetzt werden können. Man erhält stattdessen viele kleine Einzeltests, die ggf. durch die Historie des Entwicklungsprozesses mittlerweile sinnlos geworden sind, aber trotzdem nicht entfernt werden. Daher hat Roy Osherove in seinem Buch „The Art Of Unit Testing“ unter anderem den Begriff der „Unit of Work“ geprägt, um so den Fokus von Methodentests erneut auf die Funktionale Einheiten zu legen. Ein weiteres Problem liegt bei der Konzentration auf diese Art von Tests darin, dass Tests auf höheren Ebenen wie System- und Geschäftsprozesstests vernachlässigt werden. Dies führte David Heinermeier Hannson unter anderem zu seiner Aussage „TDD Is Dead“.

TDD und die User-Stories

Testgetriebene Entwicklung ist naturgemäß sehr eng mit den Anforderungen verknüpft, die durch die Software realisiert werden sollen. Hier werden in der agilen Praxis häufig User-Stories verwendet. Allerdings führt auch hier die naive Herangehensweise an diese Thematik häufig zu Qualitätsproblemen, wie in dem Artikel „A Story about User Stories and Test Driven Development“ von Gertrud Bjørnvig, James O. Coplien und Neil Harrison nachgelesen werden kann. Grund hierfür ist, dass User-Stories eigentlich nur kleine Ausschnitte in Erzählform (Beispiele) aus einem größeren Use Case sind, die eine Diskussionsgrundlage innerhalb eines agilen Teams darstellen, um die genaue in einem Entwicklungszyklus zu erstellende Funktionalität festzulegen. Durch diese kleingranulare Vorgehensweise ist es allerdings schwierig, eine gute Systemarchitektur zu entwickeln, da immer nur kleine Teilausschnitte aber nie das große Ganze betrachtet werden. Gerade in längeren Projekten wird dadurch der Refaktorisierungsaufwand bei Erstellung kleiner Features immer höher. Daher lässt sich durch TDD ein gutes Systemdesign nicht ersetzen. Die Fokussierung der Entwickler auf ihre eigene Code-(Teil-)Basis hindert sie häufig daran, den Gesamtzusammenhang zu erkennen und dementsprechend die Integrations- und Systemtests zu erstellen, die für eine gute Qualität unerlässlich sind. Die dazu benötigten Regeln und Maßnahmen werden innerhalb eines Teams nur selten definiert und umgesetzt, da die unabhängige Systemsicht fehlt.

Wieso sollte ich dann TDD einsetzen?

Die oben genannten Probleme sind nicht neu. Erfahrungsgemäß sind die meisten Gründe für das Scheitern von TDD in Projekten auf den naiven Einsatz dieser Technik zurückzuführen. Ein praktisches Beispiel, wie man testgetriebene Entwicklung betreiben kann, ist in dem Buch „Growing Object Oriented Software Guided By Tests“ von Steve Freeman und Nat Pryce beschrieben, wo sukzessiv eine Software unter Verwendung von Integrations- und Systemtests erstellt wird. Zusätzlich ist die Entwicklung mit TDD, wie es in Kent Becks Buch beschrieben ist, auch nicht stehen geblieben. Vielmehr haben sich weitere Testmethodiken entwickelt, die nicht die kleinen Funktionseinheiten im Blick haben, sondern die Business-Logik und die sich daraus ergebenden Akzeptanztests. In seinem Artikel „Introducing BDD“ hat David North beispielsweise beschrieben, wie er versucht hat, Tests in geeigneter Weise zu formulieren und wie es dadurch zur Entwicklung des Frameworks JBehave gekommen ist. Ziel war es, Business Tests zu formulieren, was leider mit klassischen xUnit-Tools nur schwer möglich ist. Eine weitere Beschreibung zu diesem Thema ist der Artikel „Acceptance TDD Explained“ von Lasse Koskela, wo er beschreibt, wie man von User-Stories zu Akzeptanztests kommt.

Trotz aller Probleme, kann TDD dabei helfen, ein qualitativ hochwertiges System zu erschaffen, wenn:

  • es nicht zu kleingranular angewendet wird,
  • die Tests nicht von einzelnen Personen formuliert werden, sondern die Sicht des großen Ganzen beinhalten,
  • die Anforderungen vom Team klar herausgearbeitet werden,
  • und der Product Owner bzw. Kunde häufig genug vor Ort ist, um Fragen zu klären, die dabei helfen, die richtigen Tests zu schreiben.