@test
assert
PHPUnit · Snapshot-Tests · PHP · Testing
Snapshots in PHP-Tests
Wann nützlich, wann gefährlich

Snapshot-Tests klingen verlockend: einmal ausführen, Ergebnis speichern, fertig. Doch in der Praxis entwickeln sie sich schnell zur Wartungslast, wenn man nicht genau versteht, für welche Szenarien sie geeignet sind und welche Disziplin im Team dafür notwendig ist.

12 Min. Lesezeit PHPUnit · spatie/snapshot-testing · JSON · HTML · Regression PHP 8.x · PHPUnit 10/11

1. Was Snapshot-Tests wirklich sind

Ein Snapshot-Test ist kein gewöhnlicher Unit-Test, der eine Erwartung gegen einen Ist-Wert prüft. Stattdessen serialisiert der Test beim ersten Ausführen die Ausgabe einer Funktion oder eines Renderers in eine Datei auf der Festplatte – den Snapshot. Bei jedem folgenden Testlauf wird die aktuelle Ausgabe gegen diesen gespeicherten Snapshot verglichen. Weicht die Ausgabe ab, schlägt der Test fehl. Das klingt simpel, hat aber weitreichende Konsequenzen für das Testdesign.

Das grundlegende Versprechen: Man muss nicht für jede Eigenschaft einer komplexen Ausgabe eine eigene Assertion schreiben. Bei einer API-Antwort mit zwanzig Feldern, bei HTML-Templates oder bei serialisierten Objektgraphen würde das dutzende Zeilen Assertionscode bedeuten. Ein Snapshot erfasst das vollständige Ergebnis in einer einzigen Datei und zeigt exakt, welche Zeile sich geändert hat – wenn er fehlschlägt. Dieses Versprechen hält Snapshot-Testing tatsächlich, aber nur unter bestimmten Bedingungen.

Der entscheidende Unterschied zu klassischen Assertions: Beim Snapshot-Test definiert der Entwickler die Erwartung nicht explizit. Er akzeptiert implizit, dass der aktuelle Zustand der Code-Ausgabe der korrekte ist. Das ist eine fundamentale Entscheidung, die gut überlegt sein will – denn ein Snapshot ist nur so gut wie der Zustand, in dem er erstellt wurde.

2. Wie Snapshots technisch funktionieren

In PHP wird Snapshot-Testing typischerweise über das Paket spatie/phpunit-snapshot-assertions realisiert. Es stellt ein Trait bereit, das in PHPUnit-Testklassen eingebunden wird und die Methode assertMatchesSnapshot() sowie typsichere Varianten wie assertMatchesJsonSnapshot(), assertMatchesHtmlSnapshot() und assertMatchesTextSnapshot() hinzufügt. Intern serialisiert das Paket den übergebenen Wert in ein Format, das in einer Datei unter __snapshots__/ neben der Testdatei gespeichert wird.

Der Dateiname des Snapshots wird aus dem Testklassennamen und der Testmethode generiert. Das ermöglicht eine klare Zuordnung: Jede Testmethode hat genau einen Snapshot – oder mehrere, wenn sie in einer Schleife aufgerufen wird, dann nummeriert. Snapshots werden versioniert und landen im Git-Repository. Das ist wichtig: Änderungen an Snapshots sind sichtbar und müssen bewusst committed werden. Das schafft Nachvollziehbarkeit, erzeugt aber auch Merge-Konflikte, wenn mehrere Entwickler gleichzeitig an Ausgaben arbeiten, die von Snapshots erfasst werden.

3. Installation und erster Snapshot

Die Installation erfolgt über Composer als Entwicklungsabhängigkeit: composer require --dev spatie/phpunit-snapshot-assertions. Das Paket unterstützt PHPUnit 10 und 11 sowie PHP 8.1 aufwärts. Nach der Installation bindet man das MatchesSnapshots-Trait in die Testklasse ein. Beim ersten Ausführen des Tests wird der Snapshot automatisch erstellt und der Test besteht. Beim zweiten Ausführen vergleicht PHPUnit die Ausgabe mit dem gespeicherten Snapshot.


<?php

declare(strict_types=1);

namespace Mironsoft\Catalog\Test\Unit;

use Mironsoft\Catalog\Service\ProductSerializer;
use PHPUnit\Framework\TestCase;
use Spatie\Snapshots\MatchesSnapshots;

final class ProductSerializerTest extends TestCase
{
    use MatchesSnapshots;

    private ProductSerializer $serializer;

    protected function setUp(): void
    {
        $this->serializer = new ProductSerializer();
    }

    /** @test */
    public function it_serializes_a_product_to_json(): void
    {
        $product = [
            'sku'   => 'TEST-001',
            'name'  => 'Test Produkt',
            'price' => 49.99,
            'stock' => 10,
        ];

        // First run: creates __snapshots__/ProductSerializerTest__it_serializes_a_product_to_json__1.json
        // Subsequent runs: compares against saved snapshot
        $this->assertMatchesJsonSnapshot(
            $this->serializer->serialize($product)
        );
    }
}

4. JSON-Snapshots für API-Antworten

JSON-Snapshots sind der häufigste und sinnvollste Einsatzbereich für Snapshot-Tests in PHP-Projekten. REST-APIs geben komplexe JSON-Strukturen zurück, die dutzende Felder enthalten können. Klassische Assertions würden jeden Feldwert einzeln prüfen – das ist bei häufig wechselnden Strukturen mühsam. Ein JSON-Snapshot erfasst die vollständige Antwort und zeigt im Fehlerfall exakt, welches Feld sich geändert hat. Das ist beim Refactoring von Serialisierungslogik besonders wertvoll: Man sieht sofort, ob eine Änderung unbeabsichtigte Nebenwirkungen auf die API-Ausgabe hat.

Wichtig beim Einsatz von JSON-Snapshots: Timestamps, UUIDs und andere dynamische Felder müssen vor dem Snapshot-Vergleich normalisiert werden. Eine API-Antwort, die ein created_at-Feld mit dem aktuellen Zeitstempel enthält, wird bei jedem Testlauf abweichen und den Snapshot ungültig machen. Die Lösung: Entweder wird das Feld aus der Antwort entfernt, bevor der Snapshot verglichen wird, oder der Service wird so strukturiert, dass Zeitstempel über eine abstrahierte Zeitquelle injiziert werden, die im Test durch eine feste Zeit ersetzt wird.

5. HTML-Snapshots für Template-Ausgaben

HTML-Snapshots sind sinnvoll für Template-Rendering in Magento-Themes oder anderen PHP-basierten Templating-Systemen. Wenn eine Block-Klasse oder ein ViewModel eine komplexe HTML-Struktur rendert, kann ein Snapshot sicherstellen, dass Refactoring die Ausgabe nicht verändert. Die Methode assertMatchesHtmlSnapshot() normalisiert den HTML-String vor dem Vergleich: Leerzeichen und Einrückung werden vereinheitlicht, sodass rein formatierungsbedingte Unterschiede keinen Snapshot-Fehler auslösen.


<?php

declare(strict_types=1);

namespace Mironsoft\Theme\Test\Unit\Block;

use Mironsoft\Theme\Block\ProductBadge;
use PHPUnit\Framework\TestCase;
use Spatie\Snapshots\MatchesSnapshots;

final class ProductBadgeTest extends TestCase
{
    use MatchesSnapshots;

    /** @test */
    public function it_renders_sale_badge_for_discounted_product(): void
    {
        $block = new ProductBadge();
        $block->setData('original_price', 99.99);
        $block->setData('final_price', 59.99);

        // Normalize timestamps and session-dependent data before snapshot
        $html = $block->toHtml();
        $html = preg_replace('/data-timestamp="\d+"/', 'data-timestamp="0"', $html);

        $this->assertMatchesHtmlSnapshot($html);
    }

    /** @test */
    public function it_renders_nothing_for_full_price_product(): void
    {
        $block = new ProductBadge();
        $block->setData('original_price', 99.99);
        $block->setData('final_price', 99.99);

        $this->assertMatchesHtmlSnapshot($block->toHtml());
    }
}

6. Wann Snapshots echten Mehrwert bringen

Snapshot-Tests sind am wertvollsten in drei konkreten Szenarien: erstens bei Legacy-Code, der keine Tests hat und bei dem das aktuelle Verhalten dokumentiert werden soll, ohne dass man jeden Output-Aspekt manuell assertieren kann. Zweitens bei stabilen Ausgaben mit vielen Feldern – API-Serializer, Exporte, PDF-Generierung – bei denen Änderungen selten und bewusst sind. Drittens bei Regressionstests nach einem Bug, bei dem die korrigierte Ausgabe einmal als korrekt bestätigt und dann eingefroren wird.

In allen drei Szenarien gilt: Der Entwickler muss den initialen Snapshot manuell verifizieren. Ein Snapshot, der auf einem fehlerhaften Stand erstellt wurde, testet nichts Sinnvolles. Er gibt lediglich Sicherheit, dass die Ausgabe so bleibt wie sie ist – auch wenn das ursprüngliche Verhalten falsch war. Dieses Risiko ist beim Einsatz für Legacy-Code besonders relevant. Hier sollte der Snapshot mit einem manuellen Review-Schritt kombiniert werden, bei dem ein zweiter Entwickler die generierte Snapshot-Datei inhaltlich prüft, bevor sie committed wird.

7. Wann Snapshots zur Falle werden

Snapshot-Tests werden gefährlich, wenn sie unkritisch accepted werden. In einem Team, das bei Snapshot-Fehlern reflexartig --update-snapshots ausführt, ohne die Diff zu lesen, existieren die Tests nur noch auf dem Papier. Der Test schlägt fehl, der Snapshot wird aktualisiert, der Commit geht durch – ohne dass jemand geprüft hat, ob die Ausgabeänderung beabsichtigt war. Dieses Muster höhlt den Wert der Tests vollständig aus, ist aber in der Praxis erschreckend häufig.

Snapshots eignen sich grundsätzlich nicht für Ausgaben, die sich bei jedem Aufruf legitimerweise unterscheiden: Zeitstempel, Zufallswerte, Session-IDs, Hash-Werte aus aktuellen Daten. Jede dynamische Komponente in der Ausgabe erfordert entweder eine Normalisierung oder schließt Snapshot-Tests aus. Ebenso problematisch: Snapshots für sehr einfache Ausgaben, die mit zwei klassischen Assertions genauso gut abgedeckt wären. Wer für einen einzelnen Stringwert einen Snapshot anlegt, baut Overhead ohne Gegenwert auf.


<?php

declare(strict_types=1);

namespace Mironsoft\Order\Test\Unit;

use Mironsoft\Order\Service\InvoiceRenderer;
use PHPUnit\Framework\TestCase;
use Spatie\Snapshots\MatchesSnapshots;

final class InvoiceRendererTest extends TestCase
{
    use MatchesSnapshots;

    /** @test */
    public function it_renders_invoice_with_normalized_dynamic_fields(): void
    {
        $renderer = new InvoiceRenderer(
            clock: new \DateTimeImmutable('2026-01-15 10:00:00') // fixed time
        );

        $invoice = $renderer->render([
            'order_id'    => 'ORD-2026-001',
            'customer'    => 'Max Mustermann',
            'total'       => 149.99,
            'tax'         => 23.96,
        ]);

        // Remove truly unpredictable values before snapshot comparison
        $normalized = preg_replace(
            ['/invoice_hash="[a-f0-9]+"/', '/generated_ms="\d+"/'],
            ['invoice_hash="HASH"',         'generated_ms="0"'],
            $invoice
        );

        $this->assertMatchesHtmlSnapshot($normalized);
    }
}

8. Snapshot-Workflow im Team und CI

Ein klarer Workflow ist die Voraussetzung, damit Snapshot-Tests ihren Nutzen entfalten und nicht zur Formalität verkommen. Der Ablauf sollte so aussehen: Snapshots dürfen nur lokal aktualisiert werden, niemals automatisch in der CI-Pipeline. In der Pipeline läuft PHPUnit ohne das --update-snapshots-Flag. Schlägt ein Snapshot-Test fehl, wird der Build rot – wie bei jedem anderen Testfehler. Die Aktualisierung des Snapshots ist dann eine bewusste Entscheidung des Entwicklers, der die Diff prüft und die Änderung als korrekt bestätigt.

In der Code-Review sollten Snapshot-Änderungen immer kommentiert werden: Warum hat sich die Ausgabe geändert? War das beabsichtigt? Entspricht der neue Snapshot der erwarteten Spezifikation? Snapshot-Dateien sind lesbarer Code-Artefakte und verdienen dieselbe Aufmerksamkeit wie Produktionscode in der Review. Teams, die das konsequent umsetzen, berichten, dass Snapshot-Tests tatsächlich Regressionsfehler aufdecken – weil die veränderte Ausgabe in der Review auffällt, obwohl der Entwickler sie nicht bewusst geändert hat.

9. Snapshots vs. klassische Assertions im Vergleich

Die Wahl zwischen Snapshot-Tests und klassischen Assertions ist keine Entweder-oder-Entscheidung. In der Praxis ergänzen sie sich. Der Vergleich macht deutlich, für welche Fälle welcher Ansatz besser geeignet ist.

Kriterium Klassische Assertions Snapshot-Tests Empfehlung
Ausgabe-Komplexität Einfache Werte, wenige Felder Komplexe JSON/HTML-Strukturen Snapshot bei >10 Feldern
Dynamische Felder Gut handhabbar Erfordert Normalisierung Klassisch für Timestamps
Lesbarkeit des Tests Klar – Erwartung im Code Erwartung in externer Datei Klassisch für Businesslogik
Wartungsaufwand Hoch bei vielen Feldern Gering bei stabilen Outputs Snapshot für stabile Serializer
Risiko Blind-Update Nicht möglich Hoch ohne Team-Disziplin Workflow-Regeln definieren

Mironsoft

PHP Testing, PHPUnit-Strategien und Testautomatisierung

PHP-Tests, die echte Sicherheit geben?

Wir analysieren bestehende Testsuiten, identifizieren Lücken und bauen eine Teststrategie auf, die Snapshot-Tests, Unit-Tests und Integrationstests sinnvoll kombiniert – mit CI-Integration und Review-Prozessen.

Test-Audit

Bestehende Testsuite auf Coverage-Lücken und falsche Teststrategien prüfen

Snapshot-Setup

Snapshot-Testing für API-Serializer und Templates einführen und dokumentieren

CI-Workflow

Snapshot-Validation in CI-Pipeline integrieren und Team-Workflow definieren

10. Zusammenfassung

Snapshot-Tests in PHP sind ein leistungsstarkes Werkzeug für spezifische Einsatzbereiche: komplexe JSON-Ausgaben, HTML-Rendering und Regressionstests für Legacy-Code. Sie reduzieren den Assertionscode erheblich und machen Output-Änderungen durch explizite Diffs sichtbar – aber nur, wenn der initiale Snapshot auf einem korrekten Zustand basiert und das Team die Disziplin hat, Snapshot-Updates zu reviewen statt blind zu akzeptieren.

Die größte Gefahr von Snapshot-Tests ist nicht technischer Natur, sondern organisatorisch: Ein Team, das --update-snapshots zur Routine macht, verliert den Schutz, den Tests bieten sollen. Der Workflow muss klar sein: Snapshots werden lokal bewusst aktualisiert, in der Review inhaltlich geprüft und in der CI niemals automatisch überschrieben. Wenn diese Regeln eingehalten werden, sind Snapshot-Tests eine wertvolle Ergänzung zu klassischen Assertions – keine Konkurrenz, sondern ein Komplement.

PHPUnit Snapshots — Das Wichtigste auf einen Blick

Einsatzbereich

Komplexe JSON/HTML-Ausgaben, Serializer, API-Antworten, Legacy-Code-Dokumentation. Nicht für dynamische Felder ohne Normalisierung.

Initialisierung

Ersten Snapshot immer manuell verifizieren. Fehlerhafter Initial-Snapshot testet nur, dass der Fehler konstant bleibt.

Team-Workflow

Updates nur lokal und bewusst. CI niemals mit --update-snapshots. Snapshot-Diffs in Code-Review inhaltlich prüfen.

Normalisierung

Timestamps, UUIDs und Zufallswerte vor dem Snapshot entfernen oder durch feste Werte ersetzen. Clock-Injection nutzen.

11. FAQ: PHPUnit Snapshots in PHP-Tests

1Was ist ein Snapshot-Test?
Ein Snapshot-Test serialisiert die Ausgabe beim ersten Lauf in eine Datei. Folgetestruns vergleichen die aktuelle Ausgabe mit diesem gespeicherten Stand. Abweichungen lösen einen Testfehler aus.
2Welches Paket für PHPUnit-Snapshots?
spatie/phpunit-snapshot-assertions. Installation: composer require --dev spatie/phpunit-snapshot-assertions. Unterstützt PHPUnit 10 und 11.
3Wie Snapshot aktualisieren?
Mit --update-snapshots beim PHPUnit-Aufruf. Nur lokal nach manueller Diff-Prüfung. Niemals automatisch in der CI-Pipeline.
4Timestamps in Snapshots handhaben?
Per Regex normalisieren, aus Output entfernen oder Clock-Injection im Service nutzen, um im Test einen fixen Zeitstempel zu setzen.
5Snapshot-Dateien ins Git?
Ja, zwingend. Snapshot-Dateien sind Teil der Testdefinition und müssen versioniert werden. Änderungen sind in git diff sichtbar und werden in der Code-Review geprüft.
6Snapshots für Magento-Templates?
Ja, für isolierbare Block-Klassen und ViewModels, die HTML rendern. Dynamische und session-abhängige Felder vorher normalisieren.
7Wann keine Snapshots verwenden?
Bei einfachen Ausgaben, stark dynamischen Inhalten ohne Normalisierung und wenn kein Review-Prozess für Snapshot-Updates vorhanden ist.
8Blinde Updates im Team verhindern?
CI-Regeln ohne --update-snapshots. Review-Pflicht für Snapshot-Dateien. Commit-Message-Konvention für Snapshot-Updates. Teamvereinbarung zur inhaltlichen Prüfung.
9PHPUnit 11 kompatibel?
Ja. spatie/phpunit-snapshot-assertions Version 5.x unterstützt PHPUnit 11. Die API ist weitgehend kompatibel zu früheren Versionen.
10Ersetzen Snapshots klassische Unit-Tests?
Nein. Snapshots ergänzen klassische Assertions. Businesslogik und Grenzfälle testet man weiterhin mit expliziten Assertions. Snapshots sind für komplexe, stabile Ausgaben gedacht.