Eine Legacy-Business-Anwendung ohne Tests sicher verändern ── Charakterisierungstests und Refactoring in der Praxis
· Go Komura · Legacy-Technologie, Nutzung vorhandener Ressourcen, Refactoring, Testdesign, Charakterisierungstest, C#, .NET, Wartung, Entscheidungstabelle, Technische Beratung
„Ich weiß genau, was ich reparieren müsste. Aber der Gedanke, dass ich durch das Anfassen etwas anderes kaputtmache, hält mich davon ab, es überhaupt anzupacken“ – das hören wir ständig von Menschen, die eine Business-Anwendung ohne Tests übernommen haben.
Viele in VB6, .NET Framework oder Access geschriebene Business-Anwendungen haben überhaupt keine automatisierten Tests. Das Spezifikationsdokument, sofern es je eines gab, wurde längst nicht mehr gepflegt, sodass „der Code ist die einzige Spezifikation“ die tatsächliche Lage ist. Und trotzdem läuft der Geschäftsbetrieb weiter, und Anfragen wie die Änderung des Mehrwertsteuersatzes, die Korrektur eines Berichtslayouts oder das Hinzufügen eines neuen Geschäftspartners warten auf niemanden.
In diesem Blog haben wir in „Wie lange läuft eine VB6-Anwendung noch? ── Ein realistischer Weg zur .NET-Migration“ die Idee vorgestellt, das alte System als „lebende Spezifikation“ zu behandeln und eine Migration voranzutreiben, indem man Ausgaben dagegen abgleicht. Dieser Artikel wendet dieselbe Idee auf eine andere Situation an: nicht das Wegmigrieren, sondern das Verändern von Code, der gerade jetzt, an Ort und Stelle, läuft. Das zentrale Werkzeug dafür ist der Charakterisierungstest. Selbst bei Code ganz ohne Tests werden Refactoring und Funktionsarbeit erheblich sicherer, wenn Sie zuerst in einem Test festschreiben, welches Verhalten Sie möglicherweise gleich brechen.
1. Zunächst das Fazit
- Reparieren Sie nicht sofort. Schreiben Sie zunächst das aktuelle Verhalten in einem Test fest. Selbst ohne Spezifikationsdokument ist die Ausgabe des aktuell laufenden Codes selbst die Spezifikation.
- Das Werkzeug dafür ist der Charakterisierungstest. Statt „korrektem Verhalten“ zeichnet er das „aktuelle Verhalten“ auf – er speichert die Ausgabe (Berichte, CSV-Dateien, Berechnungsergebnisse und so weiter) unverändert als Erwartungswert und vergleicht sie vor und nach einer Änderung (die Golden-Master-Methode).
- Für Strukturen, in die sich kein Test einschieben lässt (Logik direkt in einem UI-Eventhandler, direkte Verweise auf
DateTime.Nowoder einen Dateipfad), schaffen Sie mit der kleinstmöglichen Änderung eine Naht (Seam) – Methodenextraktion plus Schnittstelleneinschub. Ein großer Umbau ist nicht erforderlich. - Mischen Sie Refactoring und Funktionsarbeit nicht im selben Commit. Das Bestehenskriterium für Refactoring ist „null Diff“, für Funktionsarbeit „nur der beabsichtigte Diff“. Mischen Sie beides, verlieren Sie die Fähigkeit zu erkennen, was ein Diff überhaupt bedeutet.
- Wie viel Testinfrastruktur aufgebaut wird, entscheidet sich anhand von Umfang der Änderung mal verbleibender Lebensdauer des Systems mal Auswirkung eines Fehlschlags. Überall Unit-Tests einzubauen ist nicht immer die richtige Antwort – manchmal ist „nur Charakterisierungstests“ oder „nicht anfassen“ die richtige Entscheidung.
- Sie können auch ohne CI beginnen. Ein einziges Testprojekt und ein Ordner mit Erwartungswertdateien reichen aus, um Ihre Sicherheit grundlegend zu verändern, selbst wenn Sie sie nur lokal ausführen.
2. Warum „geht Legacy-Code beim Anfassen kaputt“?
Legacy-Code zu verändern ist nicht beängstigend, weil der Code alt ist, sondern weil es keine Möglichkeit gibt zu bestätigen, ob das Ergebnis einer Änderung korrekt ist.
Michael Feathers definierte in seinem Buch „Working Effectively with Legacy Code“ Legacy-Code nicht als „einfach alten Code“, sondern als „Code ohne Tests“.1 Seine Begründung: Ohne Tests gibt es keinen schnellen Weg zu bestätigen, ob der Code bei jeder Änderung besser oder schlechter geworden ist. Nach dieser Definition ist selbst gestern geschriebener Code Legacy-Code, wenn er keine Tests hat.
Bei Code ohne Tests setzt sich folgender Teufelskreis in Gang:
- Ohne Tests kennen Sie das Ausmaß der Auswirkungen einer Änderung nicht, was beängstigend ist
- Weil es beängstigend ist, vermeiden Sie es, die bestehende Struktur anzufassen, und begnügen sich mit möglichst wenig Copy-Paste plus einer zusätzlichen bedingten Verzweigung
- Diese behelfsmäßigen Korrekturen häufen sich, und der Code wird noch schwerer lesbar und noch anfälliger
- Anfälligkeit macht es noch beängstigender (zurück zu 1)
Der Ausweg aus diesem Kreislauf ist nicht „den Mut für ein großes Refactoring aufbringen“. Die Reihenfolge ist tatsächlich umgekehrt: Zuerst ein Sicherheitsnetz (Tests) aufspannen, die Ursache der Angst beseitigen und erst danach reparieren. Doch dabei gibt es ein Henne-Ei-Problem. Um einen Test zu schreiben, braucht man eine testbare Struktur. Um die Struktur testbar zu machen, muss man aber den Code ändern (refactoren). Das bedeutet, Code ohne Tests zu ändern – ohne Tests.
Um diesen Widerspruch aufzulösen, geht die Veränderung von Legacy-Code in folgender Reihenfolge vor:1
- Beschränkt auf den Bereich rund um die Änderung, das aktuelle Verhalten von außen festschreiben (ein Charakterisierungstest)
- Innerhalb dieses Sicherheitsnetzes die kleinstmögliche Änderung mit extrem geringem Risiko, etwas kaputtzumachen (etwa eine Methodenextraktion) vornehmen und so eine Stelle schaffen, an der sich ein Test einschieben lässt
- Sobald die Struktur feingranulare Tests zulässt, mit der eigentlich gewünschten Änderung (Refactoring oder Funktionsarbeit) beginnen
Die folgenden Kapitel gehen die Schritte 1 und 2 konkret durch.
3. Charakterisierungstests ── „aktuelles Verhalten“ aufzeichnen
3.1 Der Unterschied zu einem normalen Test
Ein normaler Test verifiziert korrektes Verhalten – „so soll es laut Spezifikation funktionieren“. Ein Charakterisierungstest ist anders. Er zeichnet auf, wie sich der Code gerade jetzt tatsächlich verhält, und stellt die Frage, ob das korrekt ist, bewusst zurück.
Angenommen, das Spezifikationsdokument sagt nicht, ob bei der Rundung von Bruchteilen auf- oder abgerundet oder abgeschnitten werden soll. Läuft der aktuelle Code mit Abschneiden, und läuft der Geschäftsbetrieb seit zehn Jahren so, dann ist „es wird abgeschnitten“ zumindest die faktische Spezifikation. Ein Charakterisierungstest schreibt genau das fest, in der Form „die aktuelle Ausgabe ist so-und-so“. Selbst wenn sich später herausstellt, dass es ein Bug ist, schreiben Sie es zunächst fest. Das Ändern des Verhaltens (das Beheben des Bugs) erfolgt separat, als bewusste Änderung, erst nachdem das Sicherheitsnetz steht.
3.2 Die Golden-Master-Methode Schritt für Schritt
Für Legacy-Code, dessen Ausgabe auf grober Granularität arbeitet, ist die Golden-Master-Methode die kosteneffizienteste Form des Charakterisierungstests. Das Vorgehen ist unkompliziert:
- Die Ausgabe identifizieren, die die betreffende Funktion erzeugt (Berichtstext, eine CSV-Datei, eine Liste von Berechnungsergebnissen und so weiter)
- Repräsentative Eingabedaten vorbereiten, den aktuellen Code ausführen und die Ausgabe erfassen
- Diese Ausgabe unverändert als Erwartungswertdatei (den Golden Master) speichern und ins Repository einchecken
- Ab sofort bei jeder Codeänderung den Test ausführen und bestätigen, dass der Diff zwischen Ausgabe und Erwartungswertdatei null ist
Eine C#-Implementierung benötigt keine spezielle Bibliothek – etwas so Einfaches wie das Folgende genügt:
[Fact]
public void MonatlicheAbrechnungsübersicht_GoldenMaster()
{
// 1. Eine repräsentative Eingabe einlesen (z. B. aus der Produktion entnommene, maskierte Daten)
var input = File.ReadAllLines(TestDataPath("billing-input-202606.csv"));
// 2. Die bestehende Logik unverändert aufrufen und die Ausgabezeichenkette erfassen
string actual = BillingReport.Generate(input);
// 3. Eine fehlende Erwartungswertdatei bedeutet entweder "die Testumgebung ist kaputt"
// oder "das ist der erste Lauf". So oder so nicht stillschweigend durchlassen,
// sondern nur aufzeichnen und unbedingt fehlschlagen
string expectedPath = TestDataPath("billing-expected-202606.txt");
if (!File.Exists(expectedPath))
{
File.WriteAllText(expectedPath + ".candidate", actual);
Assert.Fail("Erwartungswertdatei fehlt. Prüfen Sie den Inhalt der .candidate-Datei " +
"und committen Sie ihn als Erwartungswert, falls er korrekt aussieht.");
}
// 4. Eine exakte Übereinstimmung mit dem gespeicherten Verhalten verifizieren
string expected = File.ReadAllText(expectedPath);
Assert.Equal(expected, actual);
}
Vermeiden Sie eine Implementierung, die bei fehlender Erwartungswertdatei die aktuelle Ausgabe an Ort und Stelle als Erwartungswert speichert und den Test durchlaufen lässt. Wurde die Erwartungswertdatei nie eingecheckt oder am falschen Ort abgelegt, würde CI dann grün durchlaufen, ohne je eine Regression zu erkennen. Lassen Sie die erste Aufzeichnung wie oben eine Kandidatendatei (.candidate) ausgeben und explizit fehlschlagen, sodass der Ablauf strikt einseitig ist: Eine Person prüft sie, und erst dann wird sie als Erwartungswert eingecheckt.
Da die reine Fehlermeldung von Assert.Equal schwer auszuwerten ist, sobald ein Diff auftaucht, lohnt es sich in der Praxis, bei einem Fehlschlag zusätzlich die tatsächliche Ausgabe in eine separate Datei zu schreiben, etwa billing-actual-202606.txt, damit Sie sie mit einem Diff-Tool wie WinMerge gegen den Erwartungswert vergleichen können.
3.3 Eingaben auswählen und die Ausgabe normalisieren
Wählen Sie Eingaben nach dem Muster „repräsentativ plus Grenzfälle“. Nehmen Sie ein oder zwei normale Fälle und ergänzen Sie Eingaben, die alle Verzweigungen durchlaufen, die Sie beim Lesen des Codes gefunden haben – Monatsabschluss, null Datensätze, negative Werte, Sonderbehandlung für einen bestimmten Geschäftspartner und so weiter. Können Sie maskierte Produktionsdaten verwenden, durchlaufen diese die realen Verzweigungen am zuverlässigsten.
Normalisieren Sie nichtdeterministische Werte in der Ausgabe vor dem Vergleich. Ein Druckzeitstempel, eine Verarbeitungsdauer, eine GUID, eine automatisch vergebene Nummer und Ähnliches ändern sich bei jedem Lauf und erzeugen so unverändert bei jedem Mal einen Diff. Fügen Sie nach dem Erzeugen der Ausgabe einen Vorverarbeitungsschritt ein – etwa eine Ersetzung per regulärem Ausdruck, die Gedruckt: 2026/07/17 16:00 in Gedruckt: <DATUM> umwandelt –, bevor Sie vergleichen.
Hier eine grobe Orientierung, welche Art von Ausgabe sich gut für einen Golden Master eignet:
| Art der Ausgabe | Eignung | Anmerkung |
|---|---|---|
| CSV / Dateien mit fester Feldlänge | Ausgezeichnet | Lässt sich unverändert speichern und vergleichen. Das erste Ziel, das Sie angehen sollten |
| Berichte (Text oder die Quelldaten hinter einer Druckvorschau) | Ausgezeichnet | Die Zeichenkette kurz vor der PDF-Erzeugung erfassen; direkten Vergleich von PDF-Binärdaten vermeiden |
| Eine Liste von Berechnungsergebnissen (Beträge, Lagerbestände usw.) | Ausgezeichnet | Es ist unproblematisch, eine testexklusive Methode hinzuzufügen, die die Ergebnisse als CSV ausgibt |
| In die Datenbank geschriebener Inhalt | Gut | Den Tabelleninhalt nach dem Schreiben per SELECT abrufen, als CSV ausgeben und vergleichen |
| Die Bildschirmanzeige selbst | Eingeschränkt | Funktioniert, wenn sie sich auf eine Zeichenkette reduzieren lässt. Die Automatisierung von Bildschirmbedienungen erfordert ein anderes Werkzeugset, behandelt in „Automatisierte UI-Tests für Windows-Desktop-Anwendungen“ |
| Übertragungen an ein externes System | Eingeschränkt | Erfordert eine Naht (nächstes Kapitel), um die Daten unmittelbar vor dem Versand zu erfassen |
4. Die „Naht (Seam)“ bauen, über die sich ein Test einschieben lässt
Der Versuch, einen Golden Master zu schreiben, stößt in vielem Legacy-Code auf eine Mauer: Die Logik steht direkt in einem UI-Eventhandler, und Sie können sie nicht ausführen, ohne den Bildschirm zu starten. Was Sie hier brauchen, ist eine Stelle, an der Testcode das Verhalten ersetzen und beobachten kann – das, was Feathers eine Naht (Seam) nennt.1
4.1 Logik per Methodenextraktion von der UI lösen
Der typische „Vorher“-Zustand sieht so aus: Berechnung, Datenbankzugriff, Zeitabhängigkeit und Bildschirmaktualisierung leben alle zusammen in einem einzigen Eventhandler.
// Vorher: alles direkt im Eventhandler
private void btnCalc_Click(object sender, EventArgs e)
{
var rows = LoadRowsFromDb(); // Direkter DB-Zugriff
var now = DateTime.Now; // Abhängig von der aktuellen Zeit
decimal total = 0;
foreach (var row in rows)
{
if (row.SalesDate.Year == now.Year &&
row.SalesDate.Month == now.Month) // Nur den laufenden Monat aggregieren
{
total += Math.Floor(row.Amount * 1.1m); // Eine Geschäftsregel zur Rundung
}
}
lblTotal.Text = total.ToString("N0"); // Direkt auf dem Bildschirm angezeigt
}
So, wie es ist, erfordert das Testen der Aggregationslogik für den laufenden Monat den Bildschirm, die Datenbank und „das heutige Datum“. Der Standardweg, dies mit minimaler Änderung testbar zu machen, besteht darin, nur die Berechnung in eine Methode zu extrahieren und die externen Abhängigkeiten (die DB-Ergebnisse und die aktuelle Zeit) in Parameter zu verwandeln. Auch das Extract-Method-Refactoring von Visual Studio (Strg+R, M) reduziert dabei manuelle Umschreibfehler.2
// Nachher: nur die Berechnung wird extrahiert und nimmt "die DB-Ergebnisse"
// sowie "die aktuelle Zeit" als Parameter entgegen
internal static decimal CalcMonthlyTotal(IEnumerable<SalesRow> rows, DateTime now)
{
decimal total = 0;
foreach (var row in rows)
{
if (row.SalesDate.Year == now.Year &&
row.SalesDate.Month == now.Month)
{
total += Math.Floor(row.Amount * 1.1m);
}
}
return total;
}
private void btnCalc_Click(object sender, EventArgs e)
{
var rows = LoadRowsFromDb();
lblTotal.Text = CalcMonthlyTotal(rows, DateTime.Now).ToString("N0");
}
Die Seite des Eventhandlers besteht jetzt aus drei Zeilen – Laden, Berechnen, Anzeigen –, und die extrahierte Methode lässt sich mit beliebigen Zeilendaten und einem beliebigen Datum testen. Auch zeitbezogene Grenzfälle wie Monatsende, Monatsanfang oder ein Schaltjahr lassen sich einfach reproduzieren, indem Sie ein Datum wie new DateTime(2028, 2, 29) übergeben.
4.2 Abhängigkeiten per Schnittstelleneinschub austauschbar machen
Bei Abhängigkeiten, die sich nicht allein durch Parametrisierung lösen lassen (DateTime.Now wird überall referenziert, ein Dateipfad ist hart codiert und so weiter), kapseln Sie die Abhängigkeit hinter einer Schnittstelle und schieben diese ein. Auch die Best Practices von Microsoft Learn für .NET-Unit-Tests nennen eine direkte Abhängigkeit von DateTime.Now als klassisches Beispiel für etwas, das sich aus einem Test heraus nicht kontrollieren lässt, und beschreiben das Einkapseln in einer Schnittstelle als Mittel, um eine Naht (Seam) einzuführen.3
public interface IClock
{
DateTime Now { get; }
}
public sealed class SystemClock : IClock
{
public DateTime Now => DateTime.Now;
}
// Auf der Testseite wird eine Implementierung eingeschoben, die eine feste Zeit zurückgibt
public sealed class FixedClock : IClock
{
private readonly DateTime _fixed;
public FixedClock(DateTime value) => _fixed = value;
public DateTime Now => _fixed;
}
Fügt man IClock dem Konstruktor einer bestehenden Klasse hinzu, müssen zwangsläufig alle Aufrufstellen angepasst werden; deshalb ist es in der Übergangsphase realistisch, zusätzlich einen parameterlosen Konstruktor mit SystemClock als Standardwert bereitzustellen und die Aufrufstellen nach und nach anzupassen. Fest codierte Dateipfade oder DB-Verbindungszeichenfolgen lassen sich auf dieselbe Art hinter einer kleinen Schnittstelle kapseln, die nur die benötigten „Lese-/Schreiboperationen“ freigibt.
Zu beachten ist, dass Visual Studio auch ein integriertes Refactoring zum Extrahieren einer Schnittstelle aus einer bestehenden Klasse besitzt (Extract Interface), mit dem sich diese Art von Änderung mechanisch durchführen lässt.2
Beim Schaffen einer Naht gilt ein Grundsatz: Die Änderung, die die Naht schafft, darf das Verhalten um keinen Millimeter verändern. Sowohl Methodenextraktion als auch Schnittstelleneinschub sind mechanische, stark verhaltensbewahrende Operationen, die Compiler und IDE direkt unterstützen können. In diesem Stadium ist die Versuchung groß, „die Logik gleich mit zu korrigieren“ – widerstehen Sie dem; das ist Arbeit für die Zeit nach dem Aufspannen des Sicherheitsnetzes.
5. Eine Entscheidungstabelle dafür, wie weit man gehen sollte
Sowohl Charakterisierungstests als auch der Bau von Nähten kosten Aufwand. Alle Legacy-Code-Bereiche auf denselben Testniveau zu bringen, ist für ein kleines oder mittleres Team nicht realistisch, und es besteht auch keine Notwendigkeit dafür. Es gibt drei Entscheidungsachsen:
- Umfang der Änderung: eine Fehlerbehebung von wenigen Zeilen, eine Funktionserweiterung oder etwas, das eine strukturelle Änderung mit sich bringt
- Verbleibende Lebensdauer des Systems: für Migration oder Abschaltung in ein bis zwei Jahren vorgesehen, oder für fünf Jahre und mehr in Betrieb erwartet
- Auswirkung bei einem Fehlschlag: kosmetischer Schaden am Berichtslayout oder ein falscher Rechnungsbetrag oder Lagerbestand
| Umfang der Änderung | Verbleibende Lebensdauer | Auswirkung bei Fehlschlag | Empfohlenes Niveau |
|---|---|---|---|
| Gering (wenige Zeilen, Konfigurationswertänderung) | Kurz (bis ~2 Jahre) | Klein (kosmetische Anzeigeprobleme) | Nur Charakterisierungstests. Die betroffene Ausgabe festschreiben, ändern, null Diff bestätigen, fertig |
| Gering bis mittel | Kurz | Groß (betrifft Geld oder Lagerbestand) | Nur Charakterisierungstests, aber umfangreicher. Die Eingabemuster einschließlich Grenzfällen erweitern |
| Mittel (Funktionserweiterung, Logikänderung) | Lang (5+ Jahre) | Klein bis mittel | Charakterisierungstests plus Unit-Tests, beschränkt auf den Bereich rund um die Änderung (eine Naht bauen) |
| Mittel bis groß | Lang | Groß | Charakterisierungstests plus Unit-Test-Infrastruktur, plus Aufteilung des Releases in kleinere Einheiten |
| Groß (eine strukturelle Überarbeitung ist nötig) | Kurz | ─ | Nicht anfassen. Statt zu ändern, betrieblich umgehen und den Aufwand in Migration/Ersatz investieren |
| ─ (es gibt gar keine Änderungsanfrage) | ─ | ─ | Nicht anfassen. Bereits funktionierenden Code nicht vorsorglich refactoren |
Die beiden unteren Zeilen mit „nicht anfassen“ sind keine passive Standardwahl – sie sind eine aktive Entscheidung. In die interne Qualität eines Systems mit kurzer Restlaufzeit zu investieren, zahlt sich nicht aus. Diesen Aufwand sollten Sie stattdessen in die Migrationsentscheidung stecken, die in „Eine Entscheidungstabelle für die Verlängerung oder Migration einer VB6-/Access-Business-Anwendung“ dargelegt wird, sowie in den Entwurf des Zielsystems.
Gehen Sie zudem bis zum Aufbau von Unit-Test-Infrastruktur, müssen Sie eine Grenze ziehen zwischen dem, was in einen Unit-Test gehört, und dem, was einem Integrationstest (der auf eine echte Datenbank oder echte Dateien zugreift) vorbehalten bleiben sollte. Diese Grenze arbeiten wir als Entscheidungstabelle in „Wo die Grenze zwischen Unit-Tests und Integrationstests ziehen“ aus – es lohnt sich, diesen Artikel begleitend zu lesen. Angesichts der Tatsache, dass Unit-Tests schnell, isoliert und wiederholbar sein sollten,3 ist es ratsam, Charakterisierungstests, die eine Datenbank oder das Dateisystem berühren, in einem separaten Projekt oder einer separaten Ausführungseinheit von den Unit-Tests zu trennen.
6. Betriebsregeln ── damit das Sicherheitsnetz nicht reißt
Ein Charakterisierungstest verliert seinen Sinn leicht, wenn man die Betriebsregeln danach falsch handhabt. Hier die drei wichtigsten.
6.1 Refactoring und Funktionsarbeit nicht im selben Commit mischen
Refactoring ist eine Änderung, die Code verständlicher und wartbarer macht, ohne sein Verhalten zu ändern.4 Das Bestehenskriterium lautet also null Diff gegenüber dem Golden Master. Funktionserweiterung oder Fehlerbehebung dagegen haben als Bestehenskriterium, dass nur der beabsichtigte Diff auftaucht. Mischt man beides in einem Commit, lässt sich, sobald ein Diff auftaucht, nicht mehr feststellen, ob es „die beabsichtigte Änderung“ war oder „etwas kaputtgegangen ist“.
| Art der Änderung | Umgang mit dem Golden Master | Bestehenskriterium |
|---|---|---|
| Refactoring (strukturelle Änderung) | Nicht aktualisieren | Null Diff |
| Fehlerbehebung / Funktionserweiterung (Verhaltensänderung) | Nach einer Diff-Review aktualisieren | Nur der beabsichtigte Diff |
| Nahtbau (Methodenextraktion, Schnittstelleneinschub) | Nicht aktualisieren | Null Diff |
| Änderung der Normalisierungsregel für Erwartungswerte | Neu erzeugen | Änderungsgrund in der Commit-Nachricht angeben |
Dasselbe gilt auf Release-Ebene. Ein „Release, das nur Refactoring enthält“ soll das Verhalten unverändert lassen, sodass Sie bei einem Vorfall sofort das Refactoring in Verdacht haben können. Mischt man beides, funktioniert diese Eingrenzung nicht mehr.
6.2 Erwartungswerte in der Reihenfolge „Diff prüfen → überschreiben“ aktualisieren
Ändern Sie das Verhalten bewusst, aktualisieren Sie auch den Golden Master. Legen Sie das Vorgehen fest:
- Die Ausgabe nach der Änderung erzeugen und den Diff gegenüber dem aktuellen Erwartungswert visuell prüfen
- Bestätigen, dass der Diff nur die beabsichtigte Änderung enthält (bei auch nur einer unbeabsichtigt geänderten Zeile nachforschen)
- Die Erwartungswertdatei mit der neuen Ausgabe überschreiben und im selben Commit wie den Code in die Historie aufnehmen
Gefährlich ist das Betriebsmuster „der Test ist rot geworden, also überschreiben wir den Erwartungswert, damit er grün wird“. Tut man das, wird eine Regression unverändert als „korrekt“ aufgezeichnet, und das Sicherheitsnetz hört auf, ein Sicherheitsnetz zu sein.
6.3 Auch ohne CI eine kleinstmögliche lokal lauffähige Ausstattung aufbauen
Selbst in einem Team ohne CI-Server können Sie mit folgendem Minimalaufbau noch heute anfangen:
- Der Solution ein einziges Testprojekt hinzufügen (MSTest, NUnit und xUnit funktionieren alle, selbst unter .NET Framework)
- Erwartungswertdateien und Eingabedaten in einem
TestData-Ordner ablegen und zusammen mit dem Code versionieren - Zur Teamregel machen,
dotnet test(oder den Test Explorer von Visual Studio) vor jedem Commit manuell auszuführen - Der Release-Anleitung eine Zeile hinzufügen – „Tests ausführen und null Diff bestätigen“ –, damit niemand vergisst, das Ergebnis zu prüfen
Je besser Ihr Logging ist, desto schneller lässt sich beim Abgleich von Testergebnissen und der eigenen Ausgabe der Anwendung untersuchen. Was protokolliert werden sollte, behandeln wir in „Mindestanforderungen an einen selbst gebauten Logger und eine Checkliste für Integrationstests“.
7. Zusammenfassung
- Legacy-Code ist „Code ohne Tests“,1 und der eigentliche Grund, warum er beim Anfassen kaputtgeht, ist, dass es keine Möglichkeit gibt, das Ergebnis einer Änderung zu bestätigen. Schreiben Sie das aktuelle Verhalten in einem Test fest, bevor Sie etwas reparieren.
- Ein Charakterisierungstest zeichnet „aktuelles Verhalten“ auf, nicht „korrektes Verhalten“. Mit der Golden-Master-Methode – Berichte, CSV-Dateien oder Berechnungsergebnisse unverändert in Erwartungswertdateien speichern und vergleichen – können Sie mit reinem C#-Code beginnen.
- Für Strukturen, in die sich kein Test einschieben lässt, bauen Sie mit Methodenextraktion und Schnittstelleneinschub eine Naht. Eine Abhängigkeit wie
DateTime.Nowzu kapseln, ist eine Standardtechnik, die auch die eigene Unit-Test-Anleitung von Microsoft beschreibt.32 - Wie viel Infrastruktur aufgebaut wird, entscheidet sich anhand von Umfang der Änderung mal verbleibender Lebensdauer mal Auswirkung bei einem Fehlschlag. „Nur Charakterisierungstests“ oder „nicht anfassen“ sind beide vollkommen legitime Entscheidungen.
- Im Betrieb sollten Sie Refactoring (Bestehenskriterium ist null Diff) und Funktionsarbeit (Bestehenskriterium ist nur der beabsichtigte Diff) nicht mischen,4 und eine Aktualisierung des Erwartungswerts immer durch eine Diff-Review laufen lassen. Selbst ohne CI verändert allein die Teamzusage, Tests lokal auszuführen, die Sicherheit erheblich.
Verwandte Artikel
- Wo die Grenze zwischen Unit-Tests und Integrationstests ziehen
- Wie lange läuft eine VB6-Anwendung noch? ── Der Support-Status der Runtime und ein realistischer Weg zur .NET-Migration
- Eine Entscheidungstabelle für die Verlängerung oder Migration einer VB6-/Access-Business-Anwendung ── Behalten, Kapseln oder Ersetzen
- Mindestanforderungen an einen selbst gebauten Logger und eine Checkliste für Integrationstests
- Automatisierte UI-Tests für Windows-Desktop-Anwendungen
Verwandte Beratungsbereiche
KomuraSoft LLC (合同会社小村ソフト) übernimmt die Einführung von Charakterisierungstests in bestehende Business-Anwendungen ohne Tests, das schrittweise Refactoring hin zu einer testbaren Struktur sowie die Unterstützung bei der Entscheidung, ob in Überarbeitung oder Migration investiert werden sollte.
- Überarbeitung und Wartung bestehender Windows-Software
- Nutzung vorhandener Ressourcen und Migrationsunterstützung
- Technische Beratung / Entwurfsprüfung
- Kontakt
Referenzlinks
-
Michael C. Feathers, “Working Effectively with Legacy Code” (Prentice Hall, 2004). Zur Definition von Legacy-Code als „Code ohne Tests“, zum Vorgehen, mit einem Charakterisierungstest das aktuelle Verhalten aufzuzeichnen, bevor eine Änderung beginnt, sowie zum Konzept der Naht (Seam), über die sich ein Test einschieben lässt. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Extract and inline refactorings (Visual Studio). Zu den Bedienschritten der Visual-Studio-Refactorings Extract Method (Strg+R, M) und Extract Interface für C#/Visual Basic. ↩ ↩2 ↩3
-
Microsoft Learn, Unit testing best practices for .NET. Zu den Eigenschaften eines guten Unit-Tests (fast / isolated / repeatable / self-checking / timely), zur Technik, eine unkontrollierbare Abhängigkeit wie
DateTime.Nowin einer Schnittstelle zu kapseln, um eine Naht einzuführen, sowie dazu, Infrastrukturabhängigkeiten aus Unit-Tests herauszuhalten und stattdessen in Integrationstests zu verlagern. ↩ ↩2 ↩3 -
Microsoft Learn, Refactor code (Visual Studio). Zur Definition von Refactoring als dem Prozess, Code zu ändern, um ihn leichter zu warten, zu verstehen und zu erweitern, ohne sein Verhalten zu ändern. ↩ ↩2
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Abwärtskompatibilität von DLL- und COM-Schnittstellen — Eine Entscheidungstabelle dafür, welche Änderungen Aufrufer brechen
Welche Änderungen an einer DLL oder COM-Komponente brechen tatsächlich deren Aufrufer? Wir legen die drei Ebenen der Kompatibilität dar —...
Wie lange laufen VB6-Anwendungen noch? — Support-Status der Laufzeitumgebung und ein praxisnaher Weg zur .NET-Migration
Wie lange laufen VB6-Anwendungen noch? Dieser Artikel ordnet die Asymmetrie zwischen der Support-Richtlinie für die VB6-Laufzeitumgebung ...
Das Datenbankschema Ihrer Business-Anwendung versionieren ── Migrationspraxis gegen „jeder Kunde hat eine andere Datenbank“
Ein praktischer Leitfaden zur Versionierung des Datenbankschemas einer Business-Anwendung, deren Datenbanken über zahlreiche Kundenstando...
Wenn Sie ein System ohne Quellcode und ohne Dokumentation übernehmen — Ein praktisches Playbook, um es am Laufen zu halten
Ein praktisches Playbook für den Beginn von Betrieb und Wartung eines Geschäftssystems ohne Quellcode und ohne Spezifikationen. Behandelt...
Windows-Prozesskommunikation richtig wählen ── Eine Entscheidungstabelle für Named Pipes / TCP / gRPC / Shared Memory / COM
Wie wählt man den richtigen Weg, damit Windows-Anwendungen miteinander sprechen? Dieser Artikel ordnet Named Pipes, lokales TCP, gRPC, Sh...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Windows-App-Entwicklung
Geschäftsanwendungen, Geräteintegration und Kommunikationstools von den Anforderungen bis zur Umsetzung.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Was ist ein Charakterisierungstest?
- Es ist ein Test, der das „aktuelle Verhalten“ so, wie es ist, festhält, statt das „korrekte Verhalten“ zu prüfen. Bei Legacy-Code, bei dem das Spezifikationsdokument längst verschwunden ist, gibt es oft gar keine Möglichkeit zu prüfen, was überhaupt korrekt wäre. Deshalb speichern Sie zunächst die Ausgabe des Codes, wie er aktuell läuft (ein Bericht, eine CSV-Datei, ein Berechnungsergebnis und so weiter), als Erwartungswert und bestätigen mechanisch, dass sich die Ausgabe vor und nach einer Änderung nicht verändert hat. Das Grundmuster besteht darin, dieses Sicherheitsnetz aus festgeschriebenem Verhalten zuerst aufzuspannen und erst danach mit Refactoring oder dem Hinzufügen von Funktionen weiterzumachen.
- Wo fange ich bei Legacy-Code an, der überhaupt keine Tests hat?
- Der realistische Ansatz besteht darin, Charakterisierungstests eng auf den Bereich zu beschränken, den Sie gerade ändern wollen. Das gesamte System mit Tests abzudecken, rechnet sich in den allermeisten Fällen aufwandsmäßig nicht, und es ist auch nicht nötig. Identifizieren Sie zunächst die Ausgabe, die die betroffene Funktion erzeugt (ein Bericht, eine CSV-Datei, was in die Datenbank geschrieben wird und so weiter), speichern Sie die Ausgabe für repräsentative Eingaben in einer Datei und schreiben Sie sie fest. Führen Sie innerhalb dieses Sicherheitsnetzes kleine, verhaltensbewahrende Refactorings wie eine Methodenextraktion durch, um die Logik in eine testbare Form zu bringen, und beginnen Sie erst dann mit der eigentlich gewünschten Änderung.
- Warum sollten Refactoring und Funktionsarbeit nicht im selben Commit gemischt werden?
- Weil man, sobald ein Diff in der Ausgabe auftaucht, nicht mehr feststellen kann, was ihn verursacht hat. Refactoring wird dadurch verifiziert, dass man bestätigt, „das Verhalten hat sich nicht geändert“, während Funktionsarbeit dadurch verifiziert wird, dass man bestätigt, „nur die beabsichtigte Stelle hat sich verändert“ – die Bestehenskriterien sind gegensätzlich. Mischt man beides, lässt sich nicht mehr erkennen, ob eine Abweichung vom Golden Master „die beabsichtigte Änderung“ ist oder „etwas kaputtgegangen ist“. Sicherer ist es, sie getrennt zu halten: null Diff für einen Refactoring-Commit, nur der beabsichtigte Diff für einen Funktions-Commit.
- Wann sollte ich den Golden Master (die Erwartungswertdatei) aktualisieren?
- Nur in dem Moment, in dem Sie das Verhalten bewusst ändern – also bei einem Commit zur Funktionserweiterung oder Fehlerbehebung. Überprüfen Sie beim Aktualisieren visuell den Diff zwischen alter und neuer Ausgabe, bestätigen Sie, dass er nur die beabsichtigte Änderung enthält, und ersetzen Sie erst dann den Erwartungswert durch die neue Ausgabe. Überschreiben Sie den Erwartungswert mechanisch nur deshalb, weil der Test rot geworden ist, nehmen Sie stillschweigend eine Regression (eine unbeabsichtigte Verhaltensänderung) als „korrekt“ auf – womit der gesamte Sinn des Sicherheitsnetzes verloren geht.
Autorenprofil
Profilseite des Artikelautors.
Go Komura
Geschäftsführer von KomuraSoft LLC
Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.