Aufklärung tut Not: Nee zu TDD? - JAXenter
Perspektivenwechsel Teil 7

Aufklärung tut Not: Nee zu TDD?

Henning Wolf

In der Clique sprechen alle von TDD (Test-driven Development, also testgetriebener Entwicklung). Aber wer macht es wirklich? Viele sagen, dass es sich merkwürdig anfühlt, nicht nur beim ersten Mal. Wir fragen Dr. T. D. Dommer, einen Experten auf dem Gebiet, mit welchen Fragen und Sorgen ihn junge Entwickler und Entwicklerinnen häufig aufsuchen. Schließlich scheint die Theorie zu TDD noch ziemlich einfach, aber wie kann der Funke überspringen, der den Anfänger zum eleganten Könner macht?

Claudius A. (25) aus Hildesheim fragt: Wie soll ich eigentlich etwas testen, das noch gar nicht existiert?

Dr. T. D. Dommer antwortet : Wir haben uns daran gewöhnt, Produktivcode zu schreiben, der vorher auch nicht existiert hat. Dabei denken wir uns im Voraus einen Zusammenhang, in dem dieser Produktivcode sinnvoll erscheint, und schreiben ihn einfach hin, lassen ihn gegebenenfalls vom Compiler überprüfen. Mit testgetriebener Entwicklung machen wir einen Teil dieses Denkprozesses explizit und denken den Produktivcode aus der Sicht seines ersten Benutzers: dem Test. Wir stellen uns also beim Testen einfach mal vor, welche Klassen und Methoden wir uns wünschen würden, um einen bestimmten Sachverhalt zu testen. Auch wenn es diese Klassen und Methoden noch gar nicht gibt. So entsteht der Test zuerst. Viele Entwicklungsumgebungen erlauben uns dann das automatische Erstellen von Klassen- und Methodenrümpfen, die wir dann „nur noch“ mit Leben füllen müssen. Dies beantwortet erst einmal die Mechanik, wie wir etwas testen können, das noch nicht existiert.

Meist interessiert aber auch die Frage: Warum sollte ich so vorgehen? Hier gibt es vor allem zwei große Vorteile hervorzuheben:

  1. Der entstehende Produktivcode wird strukturiert aus der Sicht eines Benutzers der entsprechenden Klassen und Methoden. Das führt zu besserem Design, als aus der Sicht einer einzelnen Datenstruktur heraus Methoden anzubieten. Es führt zuweilen sogar zu deutlich weniger Produktivcode, weil scheinbar notwendige Methoden (was man hinzufügen kann, muss man auch löschen können) vom Test gar nicht benötigt werden. Dann sollten sie auch noch nicht erstellt werden.
  2. Methodisch hat der Ansatz der testgetriebenen Entwicklung den Vorteil, dass nur getesteter und testbarer Code entsteht. Das systematische Testen nach der Erstellung des Produktivcodes hat nämlich zwei Nachteile: Zum einen wird es oft einfach weggelassen, weil es an Disziplin mangelt oder der (Zeit-)Druck zu hoch scheint. Zum anderen merkt man dann erst beim Testen, dass der Code schlecht testbar ist und aufwändig umgebaut werden müsste. Diesen Aufwand scheut man dann aber, obwohl es allgemein als anerkannt gilt, dass schlecht testbarer Code definitiv auch schlecht entworfener Code ist. Viele heutige Systeme sind over-designed und under-tested.

Domenikus B. (29) aus Dresden zweifelt: Muss ich wirklich so kleine Schritte machen? Ich fühle mich so gebremst…

Dr. T. D. Dommer erwidert: Eine wichtige Idee hinter testgetriebener Entwicklung ist, die vollständige Kontrolle über das Programmiergeschehen zu behalten: Als Entwickler sollte ich immer wissen, ob der nächste Testlauf fehlschlagen wird oder nicht, und was ich tun muss, um einen fehlschlagenden Test wieder „grün“ zu machen. Am besten lässt sich diese Idee durchhalten, wenn ich in ganz kleinen Schritten vorgehe. In einer Minute kann ich nur Chaos für eine Minute anrichten – und dementsprechend schnell auch wieder beseitigen.

Die meisten TDD-Anfänger tendieren dazu, ihre Schritte – oft gleichbedeutend mit der Größe und Komplexität des neuesten Testfalls – stetig zu vergrößern, um schneller zu werden. Dies ist auch o.k., wenn man die Schrittweite wieder entsprechend reduziert, sobald die Kontrolle verloren geht: wenn z. B. 10 Minuten statt geschätzter zwei vergehen, bevor man alle Tests am Laufen hat. So pendelt sich die optimale Schrittweite langsam ein; erfahrene TDDler berichten meist von Mikroiterationen zwischen 2 und 5 Minuten Länge. Die Gefahr für den Test-First-Neuling ist, dass er es nicht bemerkt, wie ihm langsam die Kontrolle entgleitet und wie das geordnete Vorgehen immer mehr zu einem Stochern im Code verkommt. (O-Ton: „Vielleicht läuft der Test, wenn ich hier ‚<=‘ statt ‚<‚ benutze“.) Erwischt man sich bei einem solchen Vielleicht-Gedanken, ist die Zeit für Schrittweitenkürzung gekommen. Mein Motto lautet daher: Hier ist kürzer auf jeden Fall besser!

Ein anderer Aspekt ist die gerade am Anfang subjektiv wahrgenommene Verlangsamung. Diese rührt daher, dass die Zeit für einen Design-Test-Code-Refactor-Zyklus tatsächlich länger ist, als wenn wir lediglich den im Kopf roh vorhandenen Code hinschreiben. Dass wir jedoch durch die höhere Kontrolle die Anzahl der Verbesserungsdurchgänge reduzieren, merkt man erst nach einiger Weile. TDD sollte uns tatsächlich schon von Beginn eines Projekts an schneller machen – sobald wir die Technik beherrschen – nicht erst mittelfristig durch niedrigere Fehlerraten.

Evelyn C. (40) aus Essen hat folgende Bedenken: Unser Umfeld ist so performancekritisch, dass wir uns vorher sehr genau überlegen müssen, wie Datenstrukturen und Algorithmen aussehen. Evolutionäres Design funktioniert da nicht.

Dr. T. D. Dommer erklärt seine Sicht: Vorabdesign hat ein grundsätzliches Problem, ganz egal aus welchem Beweggrund man es tut: Es stützt sich zu einem gewissen Teil immer auf Spekulation und läuft damit Gefahr, von der Wirklichkeit überholt oder gar nutzlos gemacht zu werden. Beim Thema Performance gilt das sogar zweifach: Vorzeitige Performanceoptimierung ist ein Ratespiel hinsichtlich der beiden Fragen „Wo kommt es zu Performanceproblemen?“ und „Wie kann ich diese Probleme beseitigen?“. Bei beiden Fragen hat sich gezeigt, dass Entwickler mit ihren Vermutungen sehr häufig völlig daneben liegen. Aus meiner Sicht gibt es daher nur einen sinnvollen Ansatz: Wir halten das Design so einfach wie möglich und beginnen frühzeitig mit aussagekräftigen Messungen. Zeigen sich dann Geschwindigkeits- oder Skalierbarkeitsprobleme, nutzen wir Werkzeuge wie Profiler und Tracing, um den wirklichen Flaschenhals unseres Systems zu ermitteln. Ist das gelungen, dann schlägt jetzt die Stunde der ausgebufften Optimierer.

Zu häufig schon habe ich Systeme gesehen, die durch den Versuch der ständigen und allumfassenden Optimierung zu unwartbaren und unverständlichen Monstern geworden sind. In solcher Software lassen sich dann aufgrund ihrer Unverständlichkeit die kritischen Stellen kaum mehr identifizieren. Daher lautet meine Regel: Je wichtiger die Performance, desto mehr hüte dich vor spekulativer Optimierung!

Franziska D. (31) aus Mannheim möchte wissen: Was nützen die TDD-Unit-Tests, wenn die doch nicht das Zusammenspiel der Objekte testen?

Dr. T. D. Dommer meint dazu: Als Erstes ist mir wichtig festzustellen, dass auch der Test des Zusammenspiels aller Objekte und Komponenten seinen Platz hat. Diesem Zweck dienen Integrationstests, Subsystemtests oder Akzeptanztests. Letztlich wird auch jeder vom Anwender des Systems durchgeführte Test das Gesamtspiel überprüfen. Doch darum geht es uns bei testgetriebenem Vorgehen nicht, und das wollen wir auch ganz bewusst nicht vermischen. Wir betrachten eben genau eine Klasse als eine Unit und testen nur diese – und nicht gleichzeitig auch die Klassen, von denen sie abhängt. Dies führt uns zu besserem, entkoppelterem Design, weil eine Klasse automatisch leichter zu testen ist, wenn sie von möglichst wenig anderen Klassen abhängt – was ja gleichzeitig auch eine clevere Designregel ist. Ich erlebe häufig, dass mir Leute diese Frage stellen, deren Systeme intern so verklumpt sind, dass sich die einzelnen Klassen nur noch mit extrem hohem Aufwand überhaupt isoliert testen lassen würden. Das ist dann aber ein Designproblem, das mit konsequenter testgetriebener Entwicklung so nicht aufgetreten wäre. Das mag frustrierend sein, da es einem nicht weiterhilft, wenn das System erst einmal so verbaut ist. Es sollte einen aber gleichzeitig motivieren, nach und nach den Umbau des Legacy-Codes voranzutreiben und bei der Entwicklung neuer Teile vollständig testgetrieben vorzugehen.

Hinzufügen möchte ich noch, dass meiner Erfahrung nach ausschließlich mit (ausreichend vielen) Unit-Tests entwickelte Systeme eine sehr geringe Fehlerrate aufweisen. Obwohl theoretisch die ganzen Interaktionsszenarien nicht von diesen Testfällen erfasst werden. Anscheinend spielen dabei nicht intuitive, aber dennoch effektive Mechanismen der Fehlervermeidung eine wichtige Rolle. Das Kosten-Nutzen-Verhältnis ist für feingranulare testgetriebene Entwicklung im Allgemeinen spürbar besser als für grobe, funktionale Tests auf das Gesamtsystem. Insbesondere führt der umgekehrte Weg – nur funktionale Systemtests – sehr häufig zu stark gekoppelten und kaum modularisierten Systemen.

Das Dr.-T.-D.-Dommer-Team dankt seinen externen Beratern zum Thema testgetriebener Entwicklung, die Ihre Erfahrungen teilen mochten: Steven Collins, Christoph Kemp, Stefan Roock, Sebastian Sanitz.

Geschrieben von
Henning Wolf
Dipl.-Inform. Henning Wolf ist Geschäftsführer der it-agile GmbH in Hamburg. Er verfügt über langjährige Erfahrung aus agilen Softwareprojekten (XP, Scrum, FDD) als Entwickler, Projektleiter und Berater. Er ist Autor der Bücher „Software entwickeln mit eXtreme Programming“ und „Agile Softwareentwicklung“. Henning Wolf hilft Unternehmen und Organisationen agile Methoden erfolgreich einzuführen.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.