für sicheres Refactoring von Legacy-Code
Wer Legacy-Code refactorn will, aber keine Tests hat, steht vor einem Henne-Ei-Problem: Tests schreiben erfordert Verständnis des Codes, Verständnis erfordert Lesen des Codes, aber Sicherheit beim Ändern erfordert Tests. Golden Master Tests lösen diesen Knoten – sie testen das Verhalten, bevor es verstanden wird.
Inhaltsverzeichnis
- 1. Das Golden-Master-Konzept: Verhalten dokumentieren, nicht verstehen
- 2. Golden Master Tests in PHPUnit aufbauen
- 3. Snapshots erstellen, versionieren und aktualisieren
- 4. Refactoring-Workflow mit Golden Master Tests
- 5. Golden Master Tests in Magento 2: Preisberechnungen und Templates
- 6. Grenzen des Golden-Master-Ansatzes
- 7. Von Golden Master zu klassischen Unit-Tests
- 8. Testansätze im Vergleich
- 9. Zusammenfassung
- 10. FAQ
1. Das Golden-Master-Konzept: Verhalten dokumentieren, nicht verstehen
Ein Golden Master Test (auch Charakterisierungstest oder Approval Test genannt) erfasst die tatsächliche Ausgabe eines Systems zu einem bestimmten Zeitpunkt und speichert sie als Referenz – den "Golden Master". Bei allen künftigen Testläufen wird die aktuelle Ausgabe mit dieser Referenz verglichen. Jede Abweichung ist ein Fehlschlag. Das ist das vollständige Konzept, und seine Eleganz liegt in seiner Einfachheit.
Der entscheidende Unterschied zu klassischen Unit-Tests: Man muss nicht wissen, was das System tun sollte, nur was es tatsächlich tut. Bei Legacy-Code, dessen Verhalten über Jahre gewachsen ist und dessen Spezifikation verloren ging, ist genau das der Vorteil. Der Golden Master dokumentiert den Status quo – inklusive aller Bugs und Eigenheiten. Erst wenn der Code durch Tests abgesichert ist, können Bugs gezielt behoben werden, ohne andere Verhaltensweisen zu beschädigen.
Der Name "Golden Master" kommt aus der Massenproduktion: der Originalabzug, von dem alle Kopien gemacht werden. In der Software-Welt ist es der Referenzoutput, gegen den alle zukünftigen Versionen des Systems geprüft werden. Andere Namen für dasselbe Konzept sind Charakterisierungstest (Michael Feathers, "Working Effectively with Legacy Code"), Approval Test oder Snapshot Test.
2. Golden Master Tests in PHPUnit aufbauen
Ein Golden Master Test in PHPUnit besteht aus drei Teilen: dem Aufruf des zu testenden Codes, der Serialisierung der Ausgabe und dem Vergleich mit der gespeicherten Referenz. Die erste Herausforderung ist die Serialisierung – die Ausgabe muss so serialisiert werden, dass sie stabil und vergleichbar ist. JSON mit geordneten Schlüsseln ist für strukturierte Daten ideal. Für HTML-Ausgaben empfiehlt sich eine Normalisierung, die unwichtige Whitespace-Unterschiede ignoriert.
Beim ersten Lauf existiert noch kein Snapshot – man muss ihn erzeugen. Eine einfache Strategie: Der Test prüft, ob die Snapshot-Datei existiert. Wenn nicht, schreibt er die aktuelle Ausgabe als Snapshot und markiert den Test als übersprungen mit einer Meldung: "Snapshot erstellt, bitte überprüfen und committen." Beim nächsten Lauf vergleicht er die Ausgabe mit dem Snapshot. Diese Logik lässt sich in einer Trait- oder Basisklasse kapseln, sodass jeder Golden-Master-Test nur noch eine Zeile Boilerplate enthält.
<?php
declare(strict_types=1);
namespace Mironsoft\Tests\GoldenMaster;
use PHPUnit\Framework\TestCase;
/**
* Trait providing Golden Master / snapshot assertion helpers.
* Snapshots are stored in __snapshots__/ relative to the test file.
*/
trait GoldenMasterTrait
{
/**
* Asserts that serialized output matches stored snapshot.
* Creates the snapshot on first run (test is skipped with notice).
*/
protected function assertMatchesSnapshot(mixed $actual, string $snapshotName = ''): void
{
$snapshotName = $snapshotName ?: $this->getName();
$snapshotDir = dirname((new \ReflectionClass($this))->getFileName()) . '/__snapshots__';
$snapshotFile = $snapshotDir . '/' . $snapshotName . '.json';
$serialized = json_encode($actual, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
if (!file_exists($snapshotFile)) {
if (!is_dir($snapshotDir)) {
mkdir($snapshotDir, 0755, true);
}
file_put_contents($snapshotFile, $serialized);
$this->markTestSkipped("Snapshot created at {$snapshotFile}. Review and commit.");
}
$expected = file_get_contents($snapshotFile);
$this->assertSame(
$expected,
$serialized,
"Output does not match snapshot: {$snapshotFile}"
);
}
/**
* Updates (overwrites) an existing snapshot — call when intentional change.
*/
protected function updateSnapshot(mixed $actual, string $snapshotName = ''): void
{
$snapshotName = $snapshotName ?: $this->getName();
$snapshotDir = dirname((new \ReflectionClass($this))->getFileName()) . '/__snapshots__';
$snapshotFile = $snapshotDir . '/' . $snapshotName . '.json';
$serialized = json_encode($actual, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
file_put_contents($snapshotFile, $serialized);
}
}
3. Snapshots erstellen, versionieren und aktualisieren
Snapshots müssen ins Versionskontrollsystem eingecheckt werden – das ist eine der wichtigsten Regeln für Golden Master Tests. Ohne Versionierung verliert man die Möglichkeit, Snapshot-Änderungen im Code-Review zu überprüfen und zu sehen, wann und durch welchen Commit sich das Verhalten geändert hat. Snapshots im .gitignore zu führen ist ein Anti-Pattern, das den gesamten Nutzen des Golden-Master-Ansatzes untergräbt.
Das Aktualisieren eines Snapshots ist eine bewusste Entscheidung, nicht ein automatischer Schritt. Wenn ein Refactoring den Snapshot-Vergleich bricht, gibt es zwei Möglichkeiten: Entweder hat das Refactoring versehentlich das Verhalten geändert (Bug), oder das Verhalten hat sich absichtlich geändert und der Snapshot muss aktualisiert werden. Der Entwickler entscheidet, indem er den Snapshot-Diff überprüft. Eine --update-snapshots-Option im Test oder eine separate Methode ermöglicht das gezielte Aktualisieren.
<?php
declare(strict_types=1);
namespace Mironsoft\Tests\GoldenMaster;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Mironsoft\Pricing\PriceEngine;
/**
* Golden Master tests for PriceEngine.
* These tests capture the current pricing behaviour without asserting
* correctness — they guard against unintended changes during refactoring.
*/
#[CoversClass(PriceEngine::class)]
final class PriceEngineGoldenMasterTest extends TestCase
{
use GoldenMasterTrait;
private PriceEngine $engine;
protected function setUp(): void
{
// Inject any required fakes/stubs for deterministic output
$taxConfig = TaxConfigStub::germany();
$currencyFx = FxRatesStub::frozen();
$this->engine = new PriceEngine($taxConfig, $currencyFx);
}
#[Test]
public function priceCalculationForStandardProduct(): void
{
$product = ProductMother::standard();
$result = $this->engine->calculate($product, 'DE', 'EUR');
$this->assertMatchesSnapshot([
'net' => $result->net(),
'gross' => $result->gross(),
'tax' => $result->tax(),
'currency' => $result->currency(),
]);
}
#[Test]
public function priceCalculationForBundleProduct(): void
{
$bundle = ProductMother::bundle([
ProductMother::standard(),
ProductMother::reducedVat(),
]);
$result = $this->engine->calculate($bundle, 'DE', 'EUR');
$this->assertMatchesSnapshot([
'items' => array_map(fn($i) => ['sku' => $i->sku(), 'gross' => $i->gross()], $result->items()),
'subtotal' => $result->subtotal(),
'total' => $result->total(),
]);
}
}
4. Refactoring-Workflow mit Golden Master Tests
Der Refactoring-Workflow mit Golden Master Tests folgt einer klaren Abfolge. Erstens: Golden Master Tests für den zu refactornden Code schreiben und Snapshots erzeugen. Zweitens: Sicherstellen, dass alle Tests grün sind. Drittens: Das Refactoring durchführen – Klassen aufteilen, Methoden extrahieren, Abhängigkeiten injizierbar machen. Viertens: Testsuite laufen lassen. Wenn alle Tests grün sind, hat das Refactoring das externe Verhalten nicht geändert.
Der entscheidende psychologische Vorteil: Der Entwickler kann das Refactoring mit Vertrauen durchführen, weil er sofortige Rückmeldung über Verhaltensänderungen bekommt. Ohne Tests ist jedes Refactoring ein Glaubenssatz ("Ich habe nichts kaputt gemacht"). Mit Golden Master Tests ist es eine messbare Aussage ("Die Tests sind grün"). Das reduziert die Angst vor Legacy-Code signifikant und macht Refactorings wahrscheinlicher.
5. Golden Master Tests in Magento 2: Preisberechnungen und Templates
Magento 2 enthält komplexe Geschäftslogik in Bereichen wie Preisberechnung, Steuerlogik und Checkout-Regeln, die historisch gewachsen sind und deren Verhaltensänderungen weitreichende Konsequenzen haben. Golden Master Tests sind hier besonders wertvoll: Man definiert eine Reihe von Produktkonfigurationen, Kundensegmenten und Rabattregeln und erfasst die berechneten Preise als Snapshots. Danach kann man die Preisberechnungslogik refactorn, ohne versehentlich Steuerbeträge, Rundungsfehler oder Rabattberechnungen zu verändern.
Für Phtml-Templates ist der Golden-Master-Ansatz ebenfalls anwendbar: Man rendert ein Template mit definierten Testdaten und speichert den HTML-Output als Snapshot. Nach dem Refactoring des Templates prüft man, ob der HTML-Output semantisch identisch geblieben ist. Für HTML-Vergleiche empfiehlt sich eine Normalisierung, die Leerzeichen und Kommentare ignoriert, aber strukturelle Änderungen aufdeckt.
<?php
declare(strict_types=1);
namespace Mironsoft\Tests\GoldenMaster\Magento;
use Magento\TestFramework\Helper\Bootstrap;
use Magento\TestFramework\Fixture\DataFixture;
use Magento\Catalog\Test\Fixture\Product as ProductFixture;
use PHPUnit\Framework\Attributes\Test;
/**
* Golden Master integration test for Magento 2 tax calculation.
* Captures current tax calculation behaviour to guard against regression
* during refactoring of custom tax rule modules.
*/
#[DataFixture(ProductFixture::class, ['price' => 100.00, 'sku' => 'gm-test-001'], 'prod')]
final class TaxCalculationGoldenMasterTest extends \Magento\TestFramework\TestCase\AbstractController
{
use GoldenMasterTrait;
#[Test]
public function taxCalculationForGermanCustomer(): void
{
$objectManager = Bootstrap::getObjectManager();
$product = $this->getFixtureProduct('prod');
/** @var \Magento\Tax\Api\TaxCalculationInterface $calculator */
$calculator = $objectManager->get(\Magento\Tax\Api\TaxCalculationInterface::class);
$quoteItem = $this->buildQuoteItem($product, qty: 2, customerTaxClass: 'Retail Customer');
$result = $calculator->calculateTax($this->buildTaxDetails($quoteItem, 'DE'));
$this->assertMatchesSnapshot([
'row_tax' => round((float)$result->getRowTax(), 4),
'row_total' => round((float)$result->getRowTotal(), 4),
'row_total_incl_tax' => round((float)$result->getRowTotalInclTax(), 4),
'tax_percent' => $result->getAppliedTaxes()[0]['percent'] ?? null,
]);
}
}
6. Grenzen des Golden-Master-Ansatzes
Golden Master Tests haben klare Grenzen, die man kennen muss. Erstens dokumentieren sie nur das, was das System aktuell tut – inklusive aller Bugs. Ein Snapshot eines buggy Verhaltens ist kein Test für korrektes Verhalten, sondern ein Test für unverändertes Verhalten. Das Reparieren eines Bugs erfordert das bewusste Aktualisieren des Snapshots.
Zweitens skalieren Golden Master Tests schlecht für nicht-deterministische Ausgaben: Zeitstempel, UUIDs, Reihenfolgen aus Datenbankabfragen ohne explizites ORDER BY und zufällig generierte Tokens müssen normalisiert oder durch kontrollierbare Fakes ersetzt werden, bevor ein sinnvoller Snapshot erstellt werden kann. Drittens können Golden Master Tests falsche Sicherheit erzeugen: Ein grüner Golden Master Test bedeutet nur, dass das Verhalten unverändert ist – nicht dass es korrekt ist. Sie sind ein Sicherheitsnetz für Refactorings, kein Ersatz für Spezifikationstests.
7. Von Golden Master zu klassischen Unit-Tests
Golden Master Tests sind ein Werkzeug für eine Übergangsphase, kein dauerhafter Zustand. Das Ziel ist, nach dem Refactoring des Codes klassische Unit-Tests zu schreiben, die das gewünschte Verhalten spezifizieren, und die Golden Master Tests danach zu entfernen. Der Weg dorthin: Golden Master Tests geben die Sicherheit für das Refactoring, das den Code verständlich und testbar macht. Ist der Code sauber strukturiert, lassen sich klassische Tests schreiben, die tatsächlich Spezifikationscharakter haben.
In der Praxis bleibt in Magento-Projekten oft eine Mischung bestehen: Für stabilen Legacy-Code, der selten geändert wird, bleiben Golden Master Tests sinnvoll als Regressionsnetz. Für aktiv entwickelte Komponenten ersetzt man sie durch klassische Unit- und Integrationstests. Diese Unterscheidung sollte bewusst getroffen und dokumentiert werden, damit das Team weiß, welche Tests welchen Charakter haben.
8. Testansätze im Vergleich
Golden Master Tests, klassische Unit-Tests und Integrationstests lösen unterschiedliche Probleme. Das Verständnis dieser Unterschiede verhindert, dass Golden Master Tests als dauerhafter Ersatz für Spezifikationstests verwendet werden.
| Eigenschaft | Golden Master | Unit-Test | Integrationstest |
|---|---|---|---|
| Spezifikationscharakter | Kein — dokumentiert ist-Zustand | Hoch — testet soll-Verhalten | Mittel — testet Zusammenspiel |
| Voraussetzung | Kein Verständnis nötig | Vollständiges Verständnis nötig | Systemkenntnisse nötig |
| Bugs dokumentiert | Ja — inklusive bestehender Bugs | Nein — testet korrektes Verhalten | Nein |
| Refactoring-Sicherheit | Hoch — sofortige Rückmeldung | Hoch bei guter Abdeckung | Mittel — langsamer Feedback-Loop |
| Dauerhafter Einsatz | Für stabilen Legacy-Code | Immer | Für kritische Pfade |
Die Tabelle macht deutlich: Golden Master Tests sind für den Einstieg in Legacy-Code-Refactorings unersetzlich, aber sie sind kein Selbstzweck. Das Ziel ist immer, den Code in einen Zustand zu bringen, in dem klassische Unit-Tests möglich sind. Golden Master Tests sind das Sicherheitsnetz, das diese Transformation erlaubt.
9. Zusammenfassung
Golden Master Tests lösen das Henne-Ei-Problem bei Legacy-Code-Refactorings: Ohne Tests kein sicheres Refactoring, ohne Verständnis keine Tests. Indem man das aktuelle Verhalten als Snapshot erfasst und bei künftigen Läufen dagegen prüft, erhält man sofortige Rückmeldung über Verhaltensänderungen – ohne das Verhalten vorher verstehen zu müssen. Das gibt Entwicklern das Vertrauen, Legacy-Code anzufassen und zu verbessern.
In Magento-Projekten sind Golden Master Tests besonders wertvoll für komplexe Preisberechnungen, Steuerlogik und Template-Rendering, wo selbst kleine Verhaltensänderungen erhebliche Auswirkungen haben können. Snapshots werden ins Versionskontrollsystem eingecheckt, damit Verhaltensänderungen im Code-Review sichtbar sind. Das langfristige Ziel ist der Übergang zu klassischen Unit-Tests, die das gewünschte Verhalten spezifizieren – Golden Master Tests sind der Weg dorthin, nicht das Ziel.
Golden Master Tests — Das Wichtigste auf einen Blick
Konzept
Aktuelles Verhalten als Snapshot erfassen, bei künftigen Läufen vergleichen. Kein Verständnis des Codes nötig – nur Ausführbarkeit.
Snapshots versionieren
Immer ins VCS einchecken. Snapshot-Änderungen im Code-Review überprüfen. Nie im .gitignore führen.
Grenzen kennen
Dokumentiert ist-Zustand inklusive Bugs. Kein Ersatz für Spezifikationstests. Nicht-deterministischen Output normalisieren.
Übergang planen
Nach dem Refactoring klassische Unit-Tests schreiben. Golden Master Tests für stabilen Legacy-Code behalten, für aktive Entwicklung ersetzen.
10. FAQ: Golden Master Tests in PHPUnit
1Was ist ein Golden Master Test?
2Warum für Legacy-Code?
3Snapshots ins VCS einchecken?
4Nicht-deterministische Ausgaben?
5Wenn Golden Master Test fehlschlägt?
6Ersatz für Unit-Tests?
7Snapshot nach absichtlicher Änderung aktualisieren?
updateSnapshot()-Methode im Trait oder --update-snapshots-Option. Bewusst überschreiben und mit erklärender Commit-Message committen.8Wie lange Golden Master Tests behalten?
9Bestes Snapshot-Format?
JSON_PRETTY_PRINT für strukturierte Daten – gut lesbar im Diff. HTML für Templates. Zeitstempel und IDs normalisieren.