Ich stehe vor einer PHP-Funktion, die seit Jahren produktiv läuft. Niemand traut sich dran, weil niemand genau weiß, was sie tut. Kommentare gibt es keine, der Entwickler ist längst weg, und die Dokumentation besteht aus einer E-Mail von 2014. Das ist keine Ausnahme – das ist der Normalfall. Genau für diese Situation gibt es Charakterisierungstests.
Was Charakterisierungstests sind
Der Begriff stammt von Michael Feathers aus seinem Buch "Working Effectively with Legacy Code" (2004) – dem Standardwerk für alle, die mit gewachsenem Code arbeiten müssen. Feathers definiert Charakterisierungstests als Tests, die das tatsächliche Verhalten eines Systems dokumentieren, nicht das beabsichtigte.
Das ist ein entscheidender Unterschied. Ein normaler Unit Test prüft: "Verhält sich diese Funktion so, wie sie sich verhalten soll?" Ein Charakterisierungstest prüft: "Verhält sich diese Funktion heute genauso wie gestern?" Er testet das IST, nicht das SOLL. Ob dieses Verhalten korrekt, sinnvoll oder schön ist, interessiert dabei erst mal nicht.
Ein konkretes Beispiel
Nehmen wir eine typische gewachsene Funktion, wie sie in Legacy-Systemen überall auftaucht:
function berechneRabatt($kunde, $bestellwert, $datum) {
$rabatt = 0;
if ($kunde['typ'] == 'premium') {
$rabatt = 0.1;
if ($bestellwert > 500) {
$rabatt = 0.15;
}
}
if (date('N', strtotime($datum)) >= 6) {
$rabatt = $rabatt * 0.5;
}
if ($kunde['seit'] < '2020-01-01' && $bestellwert > 200) {
$rabatt += 0.05;
}
return round($bestellwert * $rabatt, 2);
}
Ich verstehe diese Funktion halb. Warum werden Wochenend-Rabatte halbiert? Keine Ahnung. Bevor ich sie anfasse, schreibe ich Tests, die das aktuelle Verhalten für die wichtigsten Eingabemengen festhalten:
public function testBerechneRabattPremiumKundeHoherBestellwert(): void
{
$kunde = ['typ' => 'premium', 'seit' => '2018-06-01'];
$ergebnis = berechneRabatt($kunde, 600.00, '2026-04-28'); // Montag
$this->assertSame(110.00, $ergebnis);
}
public function testBerechneRabattPremiumKundeAmWochenende(): void
{
$kunde = ['typ' => 'premium', 'seit' => '2022-01-01'];
$ergebnis = berechneRabatt($kunde, 600.00, '2026-04-25'); // Samstag
$this->assertSame(45.00, $ergebnis);
}
Der erste Test läuft grün – ich habe das Verhalten dokumentiert. Wenn er nach meinem Refactoring rot wird, habe ich das Verhalten verändert. Das ist genau der Sinn.
Vorgehen: Die kritischsten Pfade zuerst
Ich fange nicht damit an, 100% Code-Coverage zu erreichen. Ich decke die Pfade ab, die am häufigsten durchlaufen werden und bei denen ein Bug am meisten schmerzen würde. In einem Bestellsystem: die Preisberechnung, der Checkout-Prozess, die Rechnungserstellung. Alles andere kommt danach.
Ein gutes Werkzeug dabei ist Xdebug mit Coverage-Analyse: Ich schaue, welche Pfade im Produktionsbetrieb tatsächlich durchlaufen werden – und fokussiere mich genau dort.
Charakterisierungstests sind kein Qualitätssignal. Sie sagen nichts darüber aus, ob der Code das Richtige tut. Sie sagen nur: Wenn sich das Verhalten ändert, merken wir es.
Charakterisierungstests sind temporär
Das ist wichtig: Diese Tests sind Wegwerfprodukte. Sobald ich einen Code-Bereich refactored habe und weiß, was er tun soll, ersetze ich die Charakterisierungstests durch echte Tests mit klaren fachlichen Erwartungen. Die alten Tests fliegen raus. Sie haben ihren Zweck erfüllt – als Sicherheitsnetz während des Umbaus, nicht als dauerhafte Dokumentation.
Fazit
Charakterisierungstests lösen ein konkretes Problem: Wie refactore ich Code, den ich nicht vollständig verstehe, ohne dabei etwas zu brechen? Die Antwort ist: Ich dokumentiere zuerst das Verhalten, das ich vorfinde. Dann verändere ich. Dann ersetze ich die Dokumentation durch echte Tests. In dieser Reihenfolge, nicht anders.