Testgetriebene Entwicklung

Typischer testgetriebener Entwicklungsprozess

Testgetriebene Entwicklung (auch testgesteuerte Programmierung; englisch test first development oder test-driven development, TDD) ist eine Methode, die häufig bei der agilen Entwicklung von Computerprogrammen eingesetzt wird. Bei der testgetriebenen Entwicklung erstellt der Programmierer Softwaretests konsequent vor den zu testenden Komponenten.

Gründe für die Einführung einer testgetriebenen Entwicklung

Nach klassischer Vorgehensweise, beispielsweise nach dem Wasserfall- oder dem V-Modell, werden Tests parallel zum und unabhängig vom zu testenden System entwickelt oder sogar nach ihm. Dies führt oft dazu, dass der Code schwer testbar ist und somit der Aufwand für die Tests hoch ist und diese nicht die gewünschte oder erforderliche Testabdeckung erzielen. Mögliche Gründe dafür sind unter anderem:

  • Fehlende oder mangelnde Testbarkeit des Systems (monolithisch, Nutzung von Fremdkomponenten, …).
  • Verbot der Investition in nicht-funktionale Programmteile seitens der Unternehmensführung. („Arbeit, von der man später im Programm nichts sieht, ist vergeudet.“)
  • Erstellung von Tests unter Zeitdruck oder rein um die gewünschte Testabdeckung zu erzielen.
  • Nachlässigkeit und mangelnde Disziplin der Programmierer bei der Testerstellung.
  • Fokus der White-Box-Test auf das zu testende System und seine Eigenheiten und daraus resultierendes „Um Fehler herum“-Testen.

Die Methode der testgetriebenen Entwicklung versucht diesen Nachteilen der White-Box-Tests entgegenzuwirken und dabei auch ein auf die Aufgabenstellung der Software besser angepasstes und wartbareres Softwaredesign zu bekommen.

Bei der Verwendung von testgetriebener Entwicklung können im Schnitt 45 Prozent aller Fehler erkannt bzw. vermieden werden.[1] Im Vergleich dazu werden beim reinen Einsatz von Unittests im Schnitt nur 30 Prozent der Fehler erkannt.[2]

Vorgehensweise

Bei der testgetriebenen Entwicklung ist zwischen dem Testen im Kleinen Modultests (englisch unit tests) und dem Testen im Großen (Integrationstests, Systemtests, Akzeptanztests) zu unterscheiden, wobei Kent Becks Methode auf Unit-Tests ausgelegt ist.

Testgetriebene Entwicklung mit Unit-Tests (englisch middle-out TDD)

tests first

Unit-Tests werden vor dem eigentlichen Computerprogramm geschrieben. Es ist nicht festgelegt, ob der Programmierer, der die Implementierung vornehmen wird, auch die Unit-Tests erstellt. Es ist erlaubt, dass mehrere fehlschlagende Unit-Tests gleichzeitig existieren. Die Umsetzung des von einem Unit-Test geforderten Verhaltens im Computerprogramm kann zeitlich verschoben werden.

Die Methode tests first kann als Vorstufe der testgetriebenen Entwicklung betrachtet werden.

TDD nach Kent Beck

Unit-Tests und mit ihnen getestete Units werden stets parallel entwickelt. Die eigentliche Programmierung erfolgt in kleinen, wiederholten Mikroiterationen. Eine solche Iteration, die nur wenige Minuten dauern sollte, hat drei Hauptteile, die man im englischen Red-Green-Refactor nennt

  1. Red: Schreibe einen Test, der ein neues zu programmierendes Verhalten (Funktionalität) prüfen soll. Dabei fängt man mit dem einfachsten Beispiel an. Ist die Funktion schon älter, kann dies auch ein bekannter Fehler oder eine neu zu implementierende Funktionalität sein. Dieser Test wird vom vorhandenen Programmcode erst einmal nicht erfüllt, muss also fehlschlagen.
  2. Green: Ändere den Programmcode mit möglichst wenig Aufwand ab und ergänze ihn, bis er nach dem anschließend angestoßenen Testdurchlauf alle Tests besteht.
  3. Räume dann im Code auf (Refactoring): Entferne Wiederholungen (Code-Duplizierung), abstrahiere wo nötig, richte ihn nach den verbindlichen Code-Konventionen aus etc. In dieser Phase darf kein neues Verhalten – das ja dann nicht durch Tests schon abgedeckt wäre – eingeführt werden. Nach jeder Änderung werden die Tests ausgeführt, ihr Fehlschlag verbietet es, die offenbar fehlerhafte Änderung schon in den genutzten Code zu übernehmen. Ziel des Aufräumens ist es, den Code schlicht und verständlich zu machen.

Diese drei Schritte werden so lange wiederholt, bis die bekannten Fehler bereinigt sind, der Code die gewünschte Funktionalität liefert und dem Entwickler keine sinnvollen weiteren Tests mehr einfallen, welche vielleicht noch scheitern könnten. Die so behandelte programmtechnische Einheit (Unit) wird dann als einstweilen fertig angesehen. Die gemeinsam mit ihr geschaffenen Tests werden beibehalten, damit auch nach künftigen Änderungen wiederum daraufhin getestet werden kann, ob die schon erreichten Aspekte des Verhaltens weiterhin erfüllt werden.

Damit die – auch Transformationen genannten – Änderungen in Schritt 2 zum Ziel führen, muss jede Änderung zu einer allgemeineren Lösung führen; sie darf also nicht etwa nur den aktuellen Testfall auf Kosten anderer behandeln. Tests, die immer mehr ins Einzelne gehen, treiben den Code so zu einer immer allgemeineren Lösung. Die Beachtung der Transformationsprioritäten führt dabei regelmäßig zu effizienteren Algorithmen.[3]

Die konsequente Befolgung dieser Vorgehensweise ist eine evolutionäre Entwurfsmethode, indem jede der einzelnen Änderungen das System weiterentwickelt.

Testgetriebene Entwicklung mit System- oder Akzeptanztests (englisch outside-in TDD)

Systemtests werden bei testgetriebener Entwicklung immer vor dem System selbst entwickelt oder doch wenigstens spezifiziert. Aufgabe der Systementwicklung ist bei testgetriebener Entwicklung nicht mehr, wie klassisch, schriftlich formulierte Anforderungen zu erfüllen, sondern spezifizierte Systemtests zu bestehen.

Akzeptanztestgetriebene Entwicklung (ATDD) ist zwar mit testgetriebener Entwicklung verwandt, unterscheidet sich jedoch in der Vorgehensweise von testgetriebener Entwicklung.[4] Akzeptanztestgetriebene Entwicklung ist ein Kommunikationswerkzeug zwischen dem Kunden bzw. den Anwendern, den Entwicklern und den Testern, welches sicherstellen soll, dass die Anforderungen gut beschrieben sind. Akzeptanztestgetriebene Entwicklung verlangt keine Automatisierung der Testfälle, wenngleich diese fürs Regressionstesten hilfreich wäre. Die Tests bei akzeptanztestgetriebener Entwicklung müssen dafür auch für Nicht-Entwickler lesbar sein. Die Tests der testgetriebenen Entwicklung können in vielen Fällen aus den Tests der akzeptanztestgetriebenen Entwicklung abgeleitet werden.

Gemeinsamkeiten

Neben anderen Zielen werden bei allen Arten von testgetriebener Entwicklung eine möglichst vollständige Testautomatisierung angestrebt. Für testgetriebene Entwicklung müssen alle Tests einfach („per Knopfdruck“) und möglichst schnell ausgeführt werden können. Für Unit-Tests bedeutet das eine Dauer von wenigen Sekunden, für Akzeptanz- oder Systemtests von maximal einigen Minuten, bzw. nur in Ausnahmen länger.

Die großen Vorzüge der testgetriebenen Methodik gegenüber der klassischen sind:

  • Man hat eine boolesche Metrik für die Erfüllung der Anforderungen: die Tests werden bestanden oder nicht.
  • Das Refactoring, also das Aufräumen im Code, führt zu weniger Fehlern; weil man dabei in kleinen Schritten vorgeht und stets entlang bestandener Tests, entstehen dabei wenige neue Fehler, und sie sind besser lokalisierbar.
  • Weil einfach und ohne großen Zeitaufwand getestet werden kann, arbeiten die Programmierer die meiste Zeit an einem korrekten System und also mit Zutrauen und konzentriert auf die aktuelle Teilaufgabe hin. (Keine „Durchquerung der Wüste“, kein „Alles hängt mit allem zusammen“)
  • Der Bestand an automatisierten Tests dokumentiert das System, bei akzeptanzgetriebener Entwicklung sogar für Nicht-Entwickler lesbar. Man erzeugt nämlich mit den Tests zugleich eine „ausführbare Spezifikation“ – was das Softwaresystem leisten soll, liegt in Form sowohl lesbarer wie auch jederzeit lauffähiger Tests vor.
  • Ein testgetriebenes Vorgehen führt in der Tendenz zu Programmcode, der stärker modularisiert ist sowie leichter zu ändern und zu erweitern. Das geplante System wird in kleinen Arbeitsschritten entwickelt, die unabhängig geschrieben und getestet und erst danach integriert werden. Die korrespondierenden Softwareeinheiten (Klassen, Module, …) werden so kleiner, spezifischer, ihre Kopplung wird lockerer und ihre Schnittstellen werden schlichter. Nutzt man auch Mock-Objekte, zwingt dies ebenfalls dazu, Abhängigkeiten zu vermindern oder einfach zu halten, weil sonst der dabei essentielle schnelle und umstandslose Austausch von Modulen für Test- und für Produktionscode nicht möglich wäre.
  • Empirische Studien konnten eine geringere Defektrate durch TDD bei unerfahrenen Entwicklern nachweisen. Dem steht allerdings auch ein höherer Zeitaufwand gegenüber. Andere empirische Studien konnten keinen Qualitätsgewinn ermitteln. Insgesamt ergibt sich so ein uneinheitliches Bild über den tatsächlichen Qualitätsgewinn allein durch TDD.[5]

Einsatzgebiete

Testgetriebene Entwicklung ist wesentlicher Bestandteil des Extreme Programming (XP) und anderer agiler Methoden. Auch außerhalb dieser ist testgetriebene Entwicklung anzutreffen, häufig in Verbindung mit der Paarprogrammierung. Als Übungsmethode werden oft Katas eingesetzt.

Werkzeuge

Die testgetriebene Entwicklung braucht vordringlich

  • ein Werkzeug zur Build-Automatisierung wie etwa CruiseControl oder Jenkins um sicherzustellen, dass die Tests auch garantiert laufen
  • eine Integrierte Entwicklungsumgebung zu Testentwicklung und -automatisierung, damit die Iterationen schnell und unkompliziert durchlaufen werden können.

Bei der Java-Entwicklung kommen meist Ant, Maven oder Gradle und JUnit zum Einsatz. Für die meisten anderen Programmiersprachen existieren ähnliche Werkzeuge, wie z. B. für PHP PHPUnit oder für C Ceedling, Unity und CMock.[6]

Für komplexe Systeme müssen mehrere Teilkomponenten unabhängig voneinander entwickelt werden und es finden dazu auch noch Fremdkomponenten Verwendung, etwa ein Datenbanksystem zwecks persistenter Datenhaltung. Die korrekte Zusammenarbeit und Funktion der Komponenten im System muss dann auch getestet werden. Um nun die Einzelkomponenten dabei separat testen zu können, die doch aber zu ihrer korrekten Funktion wesentlich von anderen Komponenten abhängen, verwendet man Mock-Objekte als deren Stellvertreter. Die Mock-Objekte ersetzen und simulieren im Test die benötigten anderen Komponenten in einer Weise, die der Tester ihnen einprogrammiert.

Werkzeuge für Akzeptanztests und Systemtests sind beispielsweise Framework for Integrated Test oder Cucumber. Eine beliebte FIT-Variante ist Fitnesse, ein Wiki-Server mit integrierter Testerstellungs- und Testausführungsumgebung.

Kritik

Aufwand-Einwand und Gegeneinwände

Haupteinwand gegen testgetriebene Entwicklung ist der vermeintlich hohe Aufwand.

Die beschriebene Idee macht sich aber zunutze, dass der gedankliche Aufwand, der beim Programmieren in die konstruktive Beschreibung, also das Programm, investiert wird, und den Hauptteil der Programmierzeit ausmacht (im Verhältnis zum Tippaufwand etwa), eine Aufzählung einzelner zu erfüllender Punkte und Fälle beinhaltet. Mit nur wenig mehr Aufwand lässt sich also genau zu diesem Zeitpunkt vor der Programmierung der abzudeckende Fall beschreiben, das vorherige Schreiben weniger Testzeilen kann sogar zu einer besseren gedanklichen Strukturierung und höherer Codequalität führen. Zweitens führt die testgetriebene Entwicklung zu einer bestimmten Disziplin, welche Funktionen in welcher Reihenfolge implementiert werden, weil man sich erst einen Testfall überlegen muss, und damit potentiell zu einer höheren Berücksichtigung des Kundennutzens, siehe auch YAGNI.

Unit-Tests oder automatisierte Tests allgemein werden oftmals als das Sicherheitsnetz eines Programms bei notwendigen Änderungen beschrieben, ohne eine hohe Testabdeckung ist ein Softwaresystem grundsätzlich anfälliger für Fehler und Probleme in der Weiterentwicklung und Wartung.[7]

Schon bei der ersten Erstellung kann der Aufwand mit TDD bei ein wenig Übung zu der Erfüllung einer bestimmten Funktionalität also unter dem Aufwand einer vermeintlich schnellen Lösung ohne automatisierte Tests liegen. Dies gilt nach übereinstimmender Ansicht umso mehr, je langlebiger das System ist und damit wiederholt Änderungen unterliegt. Der Aufwand, automatisierte Tests nachträglich zu schreiben, ist wesentlich höher, weil gedanklich die einzelnen Anforderungen und Programmzeilen noch einmal analysiert werden müssen, und eine vergleichbare Testabdeckung wie bei TDD ist alleine aus Aufwands- und Kostengründen dann kaum noch realistisch.

Konsequenz ist nötig

Auch die Methode der testgetriebenen Entwicklung kann falsch eingesetzt werden und dann scheitern. Programmierern, die noch keine Erfahrung dabei besitzen, erscheint sie manchmal schwierig oder gar unmöglich. Sie fragen sich, wie man etwas testen soll, das doch noch gar nicht vorhanden ist. Auswirkung kann sein, dass sie die Prinzipien dieser Methode vernachlässigen, was insbesondere bei agilen Methoden wie dem Extreme Programming Schwierigkeiten beim Entwicklungsprozess oder sogar dessen Zusammenbruch zur Folge haben kann. Ohne ausreichende Unit-Tests wird keine ausreichende Testabdeckung für das Refactoring und die gewünschte Qualität erreicht. Dem kann man mit Paarprogrammierung und Schulung entgegenwirken.

Ausbildung/Übung erforderlich

Ein wesentliches Argument von Gegnern der testgetriebenen Entwicklung ist, dass insbesondere Unit-Tests den Aufwand bei Änderungen an bestehender Funktionalität unnötig erhöhen, weil eine Änderung am Produktions-Code unverhältnismäßig viele Unit-Tests fehl schlagen lässt. Die Ursache dafür liegt jedoch in der Regel darin, dass die getestete Unit nicht ausreichend separiert wurde, die Tests also nicht atomar sind.

Um dieses Problem zu vermeiden, ist es notwendig, dass die Programmierer darin geschult werden, wie sie die Anforderungen in atomare Funktionseinheiten zerlegen können und dies üben.

Kein Ersatz für alle anderen Testarten

Auch diese stark auf Tests setzende Art der Programmierung kann wie alle Testarten nicht jeden Fehler aufdecken:

  • Fehler, die im Zusammenspiel zwischen verschiedenen Programmen oder Programmteilen entstehen, können mittels Integrationstests eher gefunden werden als mittels testgetriebener Entwicklung
  • Die Gebrauchstauglichkeit einer Software kann mittels testgetriebener Entwicklung nicht festgestellt werden. Dafür sind Usability-Tests besser geeignet.
  • Die Entsprechung der Software gegenüber den funktionalen und nicht-funktionalen Anforderungen kann mittels Unit-Test TDD oft nicht festgestellt werden. Dafür ist akzeptanztestgetriebene Entwicklung wie beispielsweise Behavior Driven Development oder Systemtests anzuraten.

Keine der genannten Testarten und Vorgehensweisen kann alle Fehler aufdecken, darum sollten in den meisten Fällen mehrere Testarten und fehlervermeidende Vorgehensweisen angewendet werden.

Literatur

Einzelnachweise

  1. Capers Jones: Software Engineering Best Practices. Lessons from Successful Projects in the Top Companies. Mc Graw Hill, 2010, ISBN 978-0-07-162162-5, S. 660 (englisch, 960 S.).
  2. Steve McConnell: Code Complete. A Practical Handbook of Software Construction. 2. Auflage. Microsoft Press, 2004, ISBN 978-0-7356-1967-8, S. 470 (englisch, 960 S.): “Unit test: Lowest Rate 15%, Modal Rate 30%, Highest Rate 50%”
  3. The Transformation Priority Premise | 8th Light. 2. Februar 2016, archiviert vom Original am 25. Juni 2013; abgerufen am 24. Januar 2023.  Info: Der Archivlink wurde automatisch eingesetzt und noch nicht geprüft. Bitte prüfe Original- und Archivlink gemäß Anleitung und entferne dann diesen Hinweis.@1@2Vorlage:Webachiv/IABot/blog.8thlight.com
  4. Ken Pugh: Lean-Agile Acceptance Test-Driven Development: Better Software Through Collaboration. Addison-Wesley Professional, Boston 2011, ISBN 978-0-321-71408-4 (englisch).
  5. Andy Oram, Greg Wilson u. a.: Making Software - What Really Works And Why We Believe It. 1. Auflage. O’Reilly Media, 2010, ISBN 978-0-596-80832-7.
  6. Throw The Switch. Abgerufen am 24. Januar 2023 (amerikanisches Englisch).
  7. Robert C. Martin: Clean Code: Refactoring, Patterns, Testen und Techniken für sauberen Code. mitp-Verlag, ISBN 978-0-13-235088-4.