TDD in der modernen Softwareentwicklung

Test-driven Development (TDD) ist eine Entwicklungsmethode des Extreme Programming, bei der vor der Umsetzung des eigentlichen Programm Codes (oder Produktiv Codes) der zugehörige Testfall geschrieben wird. Jede neue Systemanforderung wird vor der Umsetzung als Testfall spezifiziert. Dieser Test schlägt zunächst fehl. Um den Test erfolgreich auszuführen, muss das entsprechende Gegenstück Produktiv Code geschrieben werden.

Die Regeln

  1. Bevor eine neue Zeile Produktiv-Code geschrieben wird, muss ein Test spezifiziert werden. Dieser Test muss fehlschlagen.
  2. Es ist nicht erlaubt, mehr als einen Test zu schreiben, der fehlschlägt.
  3. Es ist nicht erlaubt, mehr Produktiv-Code zu schreiben als notwendig, um den Test erfolgreich zu durchlaufen.

Die Ziele

Die Ziele, die mit Hilfe dieser Methode erreicht werden, sind recht vielfältig. Zum einen geht es natürlich darum, Fehler möglichst zu vermeiden bzw. möglichst schnell zu identifizieren und zu beheben. Zum anderen wird mit TDD allerdings auch generell die Qualität des Codes gesteigert, sei es durch eine bessere Modularisierung, Dokumentation oder effizientere Umsetzung.

Sicherheit

Die Zeit zur Suche nach Fehlern wird minimiert, da der Entwickler sofort Feedback zu seinen Änderungen bekommt und eine aufwendige Ursachensuche im Nachhinein entfällt. Denn je später ein Fehler gefunden wird, desto zeitaufwendiger (und damit teurer) ist es, diesen zu beheben.

Testgetriebene Entwicklung hilft, Fehlerursachen schon beim Entwickeln des Codes zu erkennen und die entsprechenden Code-Stellen daraufhin zu prüfen. Damit wird vermieden, dass zukünftige Änderungen (Erweiterungen, Refactoring, Bug Fixes) ungewollte Seiteneffekte hervorrufen. Dem Entwickler wird die Angst genommen, notwendige Verbesserungen vorzunehmen, um die Wartbarkeit zu gewährleisten und die Effizienz hochzuhalten.

Effizienz

Manuelles Testen entfällt (größtenteils). Viele, die argumentieren, TDD wäre zeitaufwendiger als herkömmliche Ansätze, vernachlässigen oft den Effekt des automatisierten Testens. Neben der Tatsache, dass automatisierte Tests schneller und ohne Personalaufwand durchführbar sind, sind diese auch deutlich gründlicher und zuverlässiger. Außerdem können sie mit geringem Aufwand in den Build-Zyklus aufgenommen werden, um fehlerhafte Änderungen sofort zu identifizieren und defekte Deployments zu vermeiden.

Wiederverwendbarkeit

Der Zwang, zu jeder Funktionalität Tests zu schreiben, führt beim Entwickler zu einer effizienteren Nutzung bereits verfügbarer Module. Statt neuen Code zu schreiben wird bestehender Code refactored und für die neuen Anforderungen angepasst bzw. erweitert. Copy-and-Paste wird verhindert, die Code-Basis bleibt weitgehend frei von Redundanzen. Die Sicherheit einer umfassenden Test-Basis erlaubt ein Refactoring ohne das enorme Risiko, bestehende Funktionalität zu beschädigen.

Entkopplung

Es ist oftmals schwierig Funktionen zu testen, die wiederum andere Funktionen aufrufen. Um diesen Code testbar zu machen ist notwendig, den Code zu isolieren, zu "entkoppeln". Diese Notwendigkeit zur Entkopplung führt zu einer Reduktion der Abhängigkeiten im Code und verhindert, dass Klassen und Module zu nicht testbaren und schwer wiederverwendbaren Monolithen heranwachsen.

Beispiel-Code

Ein großes Problem von Code-Dokumentationen ist, dass Sie zwar am Anfang recht hilfreich und aktuell sind, mit der Zeit allerdings an Aktualität verlieren. Besser wäre es, wenn die Dokumentation ebenfalls automatisiert auf Aktualität geprüft wird.

Gut geschriebener Test Code bietet eine sehr gute Ergänzung zur Dokumentation, da er wertvollen Beispiel-Code enthält. Dieser Beispiel-Code wird mit jeder Änderung erneut kompiliert und durchlaufen, so dass sichergestellt ist, dass der (Beispiel-)Code immer auf dem aktuellsten Stand ist.

Probleme zu Ende denken und Lösungen fertigstellen

Tests schreiben bedeutet, die zu entwickelnde Software aus der Client Perspektive zu erleben. Probleme werden vollständiger durchdacht und gelöst, eventuelle Unklarheiten bei der Umsetzung kommen eher zum Vorschein. Der Design eines Tests erfordert eine grobe Vorstellung darüber, wie ein bestehendes Problem aussieht und welche Schnittstellen notwendig sind, um es zu lösen.

Viele Tutorials über TDD führen in Beispielen an, wie TDD praktisch angewendet werden kann. Übersehen wird dabei oft, dass es erst dann Sinn macht, über Tests nachzudenken, wenn bereits ein konkreter Lösungsansatz für das Problem besteht. TDD ist nicht dazu da, dem Entwickler das Denken abzunehmen. TDD bedeutet auch nicht, Lösungen durch Code zu erarbeiten. Es ist viel mehr ein ergänzendes Hilfsmittel, die möglichen Lösungsansätze vorher durchzuspielen und exakt zu spezifizieren. Deswegen macht es durchaus Sinn, vor der Spezifizierung durch Tests Lösungsansätze für komplexe Probleme prototypisch durchzuspielen, um zu vermeiden, Tests für Code zu schreiben, der letztendlich nicht zielführend ist.

Fazit

Testgetriebene Entwicklung ist komplex. Es ist Einiges an Erfahrung notwengig, bevor TDD produktiv angewendet werden kann. Das Testing Framework muss bekannt sein, die Arbeitsweise an sich und die Strukturierung des Codes braucht Übung. Auch ist ein gutes Gefühl notwendig, um zu entscheiden, wann Code refactored werden sollte, um redundanten Test-Code zu vermeiden. Unerfahrene Entwickler neigen oft dazu, mit dem Testen zu früh zu beginnen, zu spät zu refactoren und/oder umständlichen Test-Code zu schreiben.

Verschiedene Studien schätzen den zusätzlichen Aufwand für TDD auf etwa 5%-35%. Die Reduktion der Fehler in der Software wird dabei um 40%-80% reduziert. Jeder kann selbst entscheiden, wieviel dies im Hinblick auf die Weiterentwicklung, Kundenzufriedenheit und Kosten für Kundenbetreueung von Wert ist. Fehler im fertigen Produkt sind für beide Seiten ärgerlich. Test können helfen, die Time-to-Market für Weiterentwicklungen ernorm zu verkürzen und dennoch ein stabiles Produkt anzubieten.

Am Ende muss jeder selbst entscheiden, ob er den Schritt gehen möchte. Wir glauben, langfristig zahlt sich diese Vorgehensweise aus den genannten Gründen aus. Für alle Beteiligten.