Ihr habt eine Codebase geerbt, die seit Jahren in Produktion läuft, aber keinen einzigen automatisierten Test hat. Jetzt sollt ihr eine Änderung einbauen – und das Gefühl in der Magengrube sagt euch: „Fass bloß nichts an, das geht schief.“ Genau dieses Szenario erleben wir bei Never Code Alone regelmäßig in unseren Consulting-Projekten. Und genau dafür gibt es eine Technik, die euch das Leben deutlich leichter macht: Characterisation Testing.
Der Begriff stammt von Michael Feathers aus seinem Buch „Working Effectively with Legacy Code“ – einem Standardwerk, das auch nach über 20 Jahren nichts an Relevanz verloren hat. Die Grundidee ist so simpel wie wirkungsvoll: Statt zu definieren, was der Code tun soll, dokumentiert ihr, was er tatsächlich tut. Ihr macht quasi einen Schnappschuss des aktuellen Verhaltens und nutzt diesen als Sicherheitsnetz für alle weiteren Änderungen.
Wir sind seit über 15 Jahren auf Softwarequalität, Open Source und Remote Consulting spezialisiert. In dieser Zeit haben wir unzählige Legacy-Systeme begleitet – von TYPO3-Monolithen über gewachsene Symfony-Anwendungen bis hin zu Frontend-Architekturen, die über Jahre ohne Teststrategie gewachsen sind. Characterisation Testing ist dabei eine der effektivsten Techniken, die wir kennen, um den ersten Schritt aus der Testlosigkeit zu machen.
Was genau ist Characterisation Testing und wie unterscheidet es sich von Unit Tests?
Diese Frage bekommen wir in fast jedem Workshop gestellt. Der entscheidende Unterschied liegt in der Perspektive. Bei klassischen Unit Tests definiert ihr vorab eine Erwartung: „Wenn ich Funktion X mit Wert Y aufrufe, erwarte ich Ergebnis Z.“ Ihr wisst, was der Code tun soll, und prüft, ob er das auch tut. Bei Characterisation Tests dreht ihr das um. Ihr ruft den Code auf, beobachtet was passiert, und schreibt genau dieses Verhalten als Test fest. Ob das Ergebnis „richtig“ ist, spielt erstmal keine Rolle – ihr dokumentiert den Ist-Zustand.
Michael Feathers formuliert das so: Der Zweck von Characterisation Testing ist es, das tatsächliche Verhalten eures Systems zu dokumentieren – nicht das Verhalten, das ihr euch wünscht. Und das ist kein Schwäche, sondern eine Stärke. Denn wenn Code in Produktion läuft und Nutzer sich auf bestimmtes Verhalten verlassen, dann ist dieses Verhalten in gewisser Weise die Spezifikation. Egal ob es ursprünglich so gedacht war oder nicht.
Ihr könnt euch das wie ein Sicherheitsnetz beim Klettern vorstellen. Es verhindert nicht, dass ihr rutscht – aber es fängt euch auf, bevor es gefährlich wird. Und genau das braucht ihr, wenn ihr Legacy Code anfassen müsst.
Wann sollte ich Characterisation Tests einsetzen statt klassischer Tests zu schreiben?
Die ehrliche Antwort: Immer dann, wenn ihr den Code nicht gut genug versteht, um klassische Tests zu schreiben. Und das ist bei Legacy Code der Normalfall, nicht die Ausnahme. In unserer Beratungspraxis sehen wir regelmäßig Codebases, in denen Methoden über hunderte Zeilen gehen, Abhängigkeiten kreuz und quer verlaufen und die ursprünglichen Entwickler längst nicht mehr verfügbar sind. In dieser Situation klassische Unit Tests schreiben zu wollen, ist wie einen Stadtplan zeichnen zu wollen, ohne die Stadt zu kennen.
Characterisation Tests sind besonders wertvoll in folgenden Situationen: Ihr müsst ein Feature in bestehendem Code ändern und habt keine Tests. Ihr plant ein Refactoring und braucht ein Sicherheitsnetz. Ihr wollt den Code verstehen, bevor ihr ihn verändert. Oder ihr übernehmt ein Projekt von einem anderen Team und müsst euch einarbeiten.
Der Clou dabei ist, dass Characterisation Tests euch einen doppelten Nutzen bringen. Einerseits bekommt ihr automatisierte Tests, die euch vor unbeabsichtigten Änderungen schützen. Andererseits lernt ihr den Code kennen, weil ihr euch aktiv damit auseinandersetzt, welche Ausgaben er für welche Eingaben produziert. Das ist deutlich effektiver als nur den Quellcode zu lesen.
Wie schreibe ich meinen ersten Characterisation Test Schritt für Schritt?
Der Prozess ist erfreulich geradlinig. Feathers beschreibt ihn als eine Art Entdeckungsreise durch den Code. Ihr startet mit einem einfachen Testfall, bei dem ihr eine Methode oder Funktion aufruft und erstmal irgendeinen Erwartungswert einsetzt – zum Beispiel einen leeren String oder eine Null. Dann lasst ihr den Test laufen. Er wird fehlschlagen, aber die Fehlermeldung verrät euch den tatsächlichen Rückgabewert. Diesen tragt ihr als Erwartungswert ein und lasst den Test erneut laufen. Jetzt sollte er grün sein.
Ein konkretes Beispiel in PHP mit PHPUnit:
public function testMaskBehavior(): void
{
// Schritt 1: Aufrufen und beobachten
$result = $this->maskService->mask('test@example.com');
// Schritt 2: Tatsächliches Ergebnis als Erwartung setzen
$this->assertEquals('****@example.com', $result);
}
Wichtig: Ihr ratet hier nicht, was der Code tun sollte. Ihr beobachtet, was er tut, und schreibt das fest. Dann wiederholt ihr den Prozess mit weiteren Eingabewerten. Was passiert bei einem leeren String? Bei einer Telefonnummer? Bei Sonderzeichen? Mit jedem neuen Testfall versteht ihr den Code ein Stück besser – und euer Sicherheitsnetz wird dichter.
Nutzt dabei unbedingt ein Code-Coverage-Tool. Es zeigt euch, welche Pfade durch euren Code bereits von Tests abgedeckt sind und wo noch Lücken bestehen. So könnt ihr gezielt weitere Testfälle ergänzen, bis ihr euch sicher genug fühlt, um mit dem Refactoring zu beginnen.
Was ist der Unterschied zwischen Characterisation Tests, Golden Master Tests und Approval Tests?
Kurze Antwort: Im Kern beschreiben alle drei Begriffe denselben Ansatz – nur mit unterschiedlichen Schwerpunkten im Namen. „Characterisation Tests“ ist der Begriff von Michael Feathers und betont, dass ihr das Verhalten des Codes charakterisiert. „Golden Master“ kommt aus der Audioindustrie und beschreibt die eine Referenzkopie, gegen die alles verglichen wird. „Approval Tests“ betont, dass ein Mensch das Ergebnis explizit genehmigt – und es auch wieder ändern kann. Und dann gibt es noch „Snapshot Tests“, die Facebook mit Jest populär gemacht hat – im Grunde dasselbe Prinzip, nur auf React-Komponenten angewandt.
Welchen Begriff ihr verwendet, ist weniger wichtig als das Verständnis des zugrundeliegenden Prinzips: Ihr erfasst das aktuelle Verhalten, speichert es als Referenz und vergleicht zukünftige Ergebnisse dagegen. Jede Abweichung wird als Änderung gemeldet – und ihr entscheidet, ob diese Änderung gewollt war oder nicht.
Aus unserer Erfahrung empfehlen wir den Begriff „Approval Tests“, weil er am besten transportiert, was passiert: Ein Mensch prüft und genehmigt das Verhalten. Das ist bei Legacy Code besonders wichtig, weil ihr dort häufig auf Verhalten stoßt, das wie ein Bug aussieht, auf das sich Nutzer aber längst verlassen. Ihr müsst dann bewusst entscheiden, ob ihr das beibehaltet oder ändert.
Wie gehe ich mit nicht-deterministischem Verhalten in Characterisation Tests um?
Das ist eine der häufigsten Stolperfallen, und sie taucht in fast jedem Legacy-Projekt auf. Nicht-deterministisches Verhalten entsteht, wenn euer Code von Dingen abhängt, die sich zwischen Testläufen ändern: Timestamps, Zufallszahlen, Datenbankabfragen, externe API-Aufrufe oder Systemumgebungsvariablen. Wenn eure Funktion heute „2026-02-12″ in die Ausgabe schreibt und morgen „2026-02-13″, werden eure Characterisation Tests ständig fehlschlagen – obwohl der Code korrekt funktioniert.
Die Lösung besteht darin, diese volatilen Elemente kontrollierbar zu machen. In der Praxis bedeutet das: Zeitabhängige Werte über eine injizierbare Clock-Abstraktion steuern, Zufallszahlen über einen Seed deterministisch machen, externe Abhängigkeiten durch Test-Doubles ersetzen und Datenbankinhalte über Fixtures stabilisieren.
Der entscheidende Punkt ist: Diese Anpassungen sollten minimal und sicher sein. Ihr wollt den Code nicht großflächig umbauen, bevor ihr Tests habt – das wäre genau das Henne-Ei-Problem, das Characterisation Testing lösen soll. Feathers nennt diese minimalen, sicheren Änderungen „Seams“ – Nahtstellen im Code, an denen ihr Verhalten für Tests austauschen könnt, ohne die Produktionslogik zu verändern.
Wenn ihr bei diesem Thema Unterstützung braucht, meldet euch bei uns. Gerade das Identifizieren und Nutzen von Seams in komplexem Legacy Code ist etwas, bei dem unsere Consulting-Erfahrung einen echten Unterschied macht. Schreibt einfach an roland@nevercodealone.de.
Welche Tools und Frameworks gibt es für Characterisation Testing?
Die gute Nachricht: Ihr braucht kein spezielles Framework. Im Prinzip könnt ihr Characterisation Tests mit jedem Test-Framework schreiben, das ihr bereits nutzt – PHPUnit, Jest, pytest, JUnit, RSpec. Der Unterschied liegt nicht im Tooling, sondern im Vorgehen.
Trotzdem gibt es spezialisierte Tools, die den Workflow deutlich komfortabler machen. Die Approval Tests Library von Llewellyn Falco ist in zahlreichen Sprachen verfügbar – darunter Java, Python, JavaScript, C#, PHP und Ruby. Sie automatisiert den Vergleich zwischen „received“ und „approved“ Ergebnis und öffnet bei Abweichungen direkt ein Diff-Tool. Für Python gibt es die approvaltests-Bibliothek, die sich nahtlos mit pytest integriert. In der JavaScript-Welt sind Jest Snapshots weit verbreitet, auch wenn sie oft zu unreflektiert eingesetzt werden. Für Java bietet sich jApprove an.
Besonders hilfreich sind Combination Approvals – ein Feature der Approval Tests Library, das automatisch viele Eingabekombinationen durchspielt und die Ergebnisse in einer übersichtlichen Textdatei sammelt. So erreicht ihr schnell eine hohe Code-Coverage, ohne jeden Testfall einzeln schreiben zu müssen.
Unser Tipp: Startet mit dem Test-Framework, das ihr bereits kennt. Der wichtigste Schritt ist, überhaupt anzufangen. Die spezialisierten Tools könnt ihr später nachrüsten, wenn ihr den Prozess verinnerlicht habt.
Taugen Characterisation Tests auch für komplexe Ausgaben wie PDFs, XML oder HTML?
Absolut – und genau hier spielen sie ihre größte Stärke aus. Stellt euch vor, euer Legacy-System generiert PDF-Rechnungen. Klassische Unit Tests, die jeden einzelnen Wert im PDF prüfen, wären extrem aufwändig und fragil. Mit einem Golden Master Ansatz speichert ihr stattdessen eine komplette Referenz-PDF und vergleicht zukünftige Generierungen dagegen.
Für HTML-Ausgaben – zum Beispiel von Template-Engines oder API-Responses – funktioniert das genauso. Ihr erfasst die komplette Ausgabe als Referenz und lasst euer Tool bei jeder Änderung den Unterschied anzeigen. Das ist besonders praktisch bei Refactoring-Projekten, in denen ihr sicherstellen müsst, dass die Ausgabe identisch bleibt, obwohl sich der darunterliegende Code ändert.
Bei XML und JSON bietet sich an, die Ausgabe vor dem Vergleich zu normalisieren – also zu sortieren und einheitlich zu formatieren. So vermeidet ihr Fehlalarme durch irrelevante Reihenfolge-Unterschiede. Viele Approval-Testing-Tools bringen dafür bereits Scrubber und Normalizer mit.
Aus unserer Projekterfahrung können wir sagen: Gerade bei komplexen Ausgabeformaten ist Characterisation Testing oft die einzig realistische Möglichkeit, überhaupt zu einer belastbaren Testabdeckung zu kommen. Der Versuch, solche Ausgaben mit einzelnen Assertions zu prüfen, führt zu unlesbarem und unwartbarem Testcode.
Wie setze ich Characterisation Tests bei einem großen Refactoring-Projekt strategisch ein?
Michael Feathers beschreibt den „Legacy Code Change Algorithm“ in vier Schritten: Identifiziert die Stelle im Code, die ihr ändern müsst. Findet Testpunkte rund um diese Stelle. Schreibt Characterisation Tests für den betroffenen Bereich. Macht eure Änderung und prüft, ob die Tests weiterhin bestehen.
Der Schlüssel liegt im Wort „gezielt“. Ihr müsst nicht die gesamte Codebase mit Characterisation Tests abdecken – das wäre bei einem großen System weder praktikabel noch nötig. Konzentriert euch auf den Bereich, den ihr tatsächlich ändern wollt. Schreibt so viele Tests, wie ihr braucht, um das Verhalten in diesem Bereich zu verstehen und abzusichern.
In unseren Consulting-Projekten empfehlen wir einen iterativen Ansatz: Startet mit den groben, äußeren Tests – zum Beispiel auf API- oder Controller-Ebene. Die geben euch schnell ein breites Sicherheitsnetz. Dann arbeitet ihr euch nach innen vor und schreibt feingranularere Tests für die spezifischen Bereiche, die ihr refactoren wollt. Nach dem Refactoring könnt ihr die groben Characterisation Tests schrittweise durch intention-revealing Unit Tests ersetzen, die das gewünschte Verhalten klar beschreiben.
Dieser Ansatz ist kein Sprint, sondern ein Marathon. Aber er funktioniert – und er ist sicherer als jede Alternative, die wir kennen. Wenn ihr ein Refactoring-Projekt plant und dabei Begleitung braucht, sprecht uns an.
Kann ich Characterisation Tests automatisch generieren lassen?
Ja, teilweise. Da Characterisation Tests auf der Beobachtung von tatsächlichem Verhalten basieren, lässt sich der Prozess durchaus automatisieren. Ihr könnt ein Skript schreiben, das euren Code mit einer großen Bandbreite an Eingabewerten aufruft, die Ausgaben speichert und daraus Testfälle generiert. Die Approval Tests Library unterstützt das mit den bereits erwähnten Combination Approvals.
Für Java gibt es JUnit Factory, das automatisch Characterisation Tests generieren kann. Und auch moderne KI-Tools wie Claude Code oder GitHub Copilot können beim Erstellen von Characterisation Tests unterstützen – allerdings mit der wichtigen Einschränkung, dass ihr die generierten Tests verstehen und validieren müsst. Automatische Generierung ersetzt nicht das menschliche Verständnis.
Aus unserer Erfahrung ist ein hybrider Ansatz am effektivsten: Nutzt automatische Generierung, um schnell eine Basis-Abdeckung zu erreichen. Ergänzt dann manuell gezielte Tests für die Bereiche, die euch besonders kritisch erscheinen. Und investiert Zeit darin, die automatisch generierten Tests zu reviewen – denn dabei lernt ihr den Code kennen, was mindestens so wertvoll ist wie die Tests selbst.
Wann sollte ich Characterisation Tests durch „echte“ Unit Tests ersetzen?
Das ist eine berechtigte Frage, denn Characterisation Tests sind als Übergangs-Werkzeug gedacht – nicht als Dauerlösung. Sie haben bewusst Limitierungen: Sie dokumentieren nicht die Intention des Codes, sie können irreführend sein wenn sie Bugs konservieren, und sie neigen dazu, gedankenlos aktualisiert zu werden, ohne die Änderung wirklich zu prüfen.
Der richtige Zeitpunkt für den Übergang ist nach dem Refactoring. Wenn ihr den Code umstrukturiert habt und seine Intention jetzt versteht, könnt ihr die Characterisation Tests durch sprechende Unit Tests ersetzen. Statt „assertiert, dass f(3.14) gleich 42 ist“ schreibt ihr dann „berechnet den Steuersatz korrekt für den regulären Satz“. Der Test prüft dasselbe Verhalten, aber er erklärt auch warum.
In der Praxis empfehlen wir: Behaltet die äußeren Characterisation Tests als zusätzliches Sicherheitsnetz, solange ihr am Refactoring arbeitet. Ersetzt sie schrittweise von innen nach außen. Und löscht sie erst, wenn ihr sicher seid, dass eure neuen Tests denselben Schutz bieten.
Und noch ein Hinweis zum Umgang mit Bugs, die ihr beim Characterisieren entdeckt: Nicht sofort fixen. Dokumentiert sie, besprecht sie mit dem Team und mit den Stakeholdern. Manchmal stellt sich heraus, dass ein vermeintlicher Bug ein Feature ist, auf das sich Nutzer verlassen. Manchmal ist es tatsächlich ein Bug, aber das Timing für den Fix muss mit dem Product Owner abgestimmt werden.
Euer nächster Schritt
Characterisation Testing ist keine Raketenwissenschaft – aber es braucht Übung und ein gutes Verständnis für die Fallstricke. Gute Einstiegspunkte zum Üben sind die Gilded Rose Kata und die Tennis Refactoring Kata von Emily Bache. Beide Repositories sind speziell dafür designt, Characterisation Testing und Refactoring in einer sicheren Umgebung zu trainieren.
Wenn ihr in eurem Projekt vor der Herausforderung steht, Legacy Code sicher zu modernisieren, und dabei professionelle Unterstützung sucht, sind wir der richtige Ansprechpartner. Wir bringen die Erfahrung mit, euch durch den Prozess zu begleiten – vom ersten Characterisation Test bis zum sauberen, getesteten Code.
Schreibt uns einfach eine Mail an roland@nevercodealone.de – wir freuen uns auf euer Projekt.
Never Code Alone – Gemeinsam für bessere Software-Qualität!
