Funktioniert Test-Driven Development auch mit Legacy-Applikationen? Diese Frage bekomme ich häufig, und die kurze Antwort lautet: Ja. Im Video nehme ich eine grosse, hässliche WinForms-Anwendung und zeige Schritt für Schritt, wie ich mit TDD ein neues Feature ergänze, ohne den bestehenden Spaghetti-Code anfassen zu müssen. Das Ziel ist einfach: demonstrieren, dass der Test-First-Mindset auch dann funktioniert, wenn die Codebasis drumherum überhaupt keine Tests kennt.
Der Ausgangspunkt: Eine grosse, hässliche WinForms-Anwendung#
Die Anwendung im Video ist genau die Art von Code, mit der viele Entwicklerinnen und Entwickler jeden Tag zu tun haben. Eine Legacy-WinForms-Anwendung mit vielen Einstellungen und Screens, ohne Testprojekt, ohne nennenswerte Testabdeckung. Wenn ich sie in Visual Studio öffne und starte, läuft sie zwar, aber es gibt nichts, das mir Sicherheit geben würde, sobald ich etwas ändere.
Der Task ist bewusst klein und realistisch: Ein neuer Button auf der Form, und wenn man darauf klickt, soll ein Label mit einem Text erscheinen. Genau bei solchen Tickets sind viele versucht, die Tests wegzulassen, weil es “nur ein Button” ist. Aber genau hier fängt das Muster an.
Regel Nummer eins: Immer mit einem Test beginnen#
Das Erste, was ich bei Test-Driven Development mache, ist einen Test einführen. Diese Regel ändert sich nicht, nur weil die Anwendung Legacy ist. Da das Projekt noch kein Testprojekt hat, lege ich ein neues Unit-Test-Projekt neben dem Legacy-Projekt an und nenne es “Big Legacy Project Test”. Das ist der erste konkrete Schritt: der Legacy-Solution überhaupt einen Ort geben, an dem Tests leben können.
Im Testprojekt erstelle ich eine erste Testklasse. Ich fange nicht mit Produktionscode an. Ich ziehe keinen Button auf die Form. Ich überlege mir zuerst, wie sich das neue Verhalten von aussen anfühlen soll.
Das Backend vom Test her designen#
Bevor ich das WinForms-UI anfasse, entscheide ich mich, die neue Funktionalität in einer separaten Klasse namens Backend unterzubringen. Das UI wird später darauf zugreifen, aber die Tests treiben das Design.
Also benenne ich die Testklasse in BackendTest um und schreibe eine Testmethode. Im Test arrangiere ich ein neues Backend, rufe im Act-Schritt eine Methode OnClick auf, und im Assert prüfe ich, dass das Ergebnis gleich "hello world" ist. Ich füge die klassischen Arrange-, Act-, Assert-Kommentare hinzu, damit die Absicht des Tests sofort klar ist.
In diesem Moment existiert die Klasse Backend noch gar nicht, und das Testprojekt hat nicht einmal eine Referenz auf das Legacy-Projekt. Das ist in Ordnung. Die Compiler-Fehler sagen mir genau, was als Nächstes zu tun ist: die Klasse Backend im Legacy-Projekt erstellen, public machen und eine Projektreferenz vom Testprojekt auf das Legacy-Projekt setzen, damit der Test sie überhaupt sieht.
Das ist der entscheidende Perspektivenwechsel. In einer Legacy-Anwendung ist die Versuchung gross, sofort den Form-Designer zu öffnen, den Button draufzuziehen und alles direkt zu verdrahten. Mit TDD zieht der Test den neuen Code Schritt für Schritt ins Leben.
Red, Green, Refactor in der Praxis#
Ab hier läuft der klassische Red-Green-Refactor-Zyklus.
Zuerst Red: Ich führe den Test aus, und er schlägt mit einer NotImplementedException fehl, weil Visual Studio einen Stub für OnClick generiert hat, der wirft. Genau das will ich. Ein fehlgeschlagener Test beweist, dass der Test wirklich läuft und wirklich etwas prüft.
Dann Green: Ich ändere OnClick so, dass die Methode "hello world" zurückgibt. Absichtlich baue ich zuerst einen kleinen Fehler ein und gebe "hello world!" mit Ausrufezeichen zurück. Ich lasse die Tests erneut laufen und sie bleiben rot, weil der erwartete Wert kein Ausrufezeichen enthält. Ich entferne es, führe die Tests noch einmal aus, und jetzt werden sie grün. Dieser kleine Umweg ist nützlich. Er beweist, dass die Assertion wirklich die Strings vergleicht.
Dann Refactor: Mit einem grünen Test im Rücken kann ich die OnClick-Methode in Ruhe anschauen und aufräumen. Der Test sagt mir sofort Bescheid, wenn ich das Verhalten kaputt mache.
Das Legacy-UI an den getesteten Code anbinden#
Erst jetzt gehe ich zurück in die WinForms-Form und füge den neuen Button hinzu. Ich nenne ihn “Click me” und ergänze einen Button-Click-Handler. Im Handler instanziere ich das Backend und rufe OnClick auf, danach schreibe ich den zurückgegebenen Text in label1.Text.
Ich sage es im Video ganz klar: Das ist kein schöner Code. Das Backend direkt im Click-Handler zu instanziieren, ist nicht der Zustand, in dem ich es langfristig lassen würde. Aber das Entscheidende ist: Die Logik, die den Text erzeugt, liegt in einer Klasse, die von einem Test abgedeckt ist. Der hässliche WinForms-Glue-Code ist dünn, und das Verhalten darunter ist sicher.
Ich lasse alle Tests noch einmal laufen, alles bleibt grün. Ich starte die Anwendung, klicke auf den Button, und das Label zeigt "hello world". Das Feature ist fertig, und die Legacy-Anwendung hat jetzt ihren ersten echten Test.
Was das für Legacy-Projekte bedeutet#
Die Frage zu Beginn des Videos war, ob man TDD innerhalb einer Legacy-Anwendung, innerhalb einer Rich-Client-Anwendung, innerhalb von etwas WinForms-artigem und wirklich Hässlichem einsetzen kann. Die Antwort ist ja. Du musst nicht erst die ganze Codebasis refactoren. Du musst nicht auf den grossen Rewrite warten. Du kannst ein Testprojekt neben das bestehende Projekt legen, neue Logik in eine kleine, testbare Klasse herausziehen und diese Klasse von Tag eins an mit Tests treiben.
Die Magie hinter Test-Driven Development ist, dass du immer mit einem Test anfängst. Das ist der Test-First-Mindset. Sobald du ihn verinnerlichst, hört der Zustand des bestehenden Codes auf, eine Ausrede zu sein. Jedes neue Feature wird zur Gelegenheit, innerhalb der Legacy-Anwendung eine Insel aus getestetem, vertrauenswürdigem Code wachsen zu lassen.
Wichtigste Erkenntnisse#
TDD funktioniert auch mit Legacy Code. Ein fehlendes Testprojekt ist kein Blocker, sondern das Erste, was du anlegst. Ein Unit-Test-Projekt neben dem Legacy-Projekt, und schon hast du einen Startpunkt.
Lass den Test das Design treiben. Überlege dir, wie sich das neue Verhalten von aussen anfühlen soll, schreibe den Test zuerst und lass dich von den Compiler-Fehlern zu den Klassen und Methoden führen, die du brauchst.
Neue Logik raus aus dem UI. Pack die neue Funktionalität in eine einfache Klasse wie
Backend, und lass das Legacy-UI darauf zugreifen. Der UI-Glue bleibt dünn, die Logik darunter ist durch Tests abgedeckt.Vertraue dem Red-Green-Refactor-Zyklus. Ein fehlschlagender Test, eine minimale Implementierung bis er grün wird, dann sauberes Aufräumen. Selbst ein kleiner Fehler wie ein zusätzliches Ausrufezeichen wird zu einem nützlichen Check, dass deine Assertions wirklich greifen.
Verinnerliche den Test-First-Mindset. Die eigentliche Veränderung ist nicht das Tooling, sondern dass du immer mit einem Test anfängst. Sobald das dein Default ist, skaliert TDD ganz natürlich von Greenfield-Projekten bis in die hässlichsten Legacy-Anwendungen.
