@test
assert
PHPUnit · Magento 2 · Fixtures · @magentoDataFixture
Magento Fixtures in Integrationstests verstehen
@magentoDataFixture, Rollback und Fixture-Klassen

Ohne saubere Fixtures sind Integrationstests fragil: Tests erzeugen Testdaten, die den nächsten Test beeinflussen, Rollbacks schlagen fehl und das Fixture-Skript ist an einem anderen Pfad als erwartet. Das Magento Test Framework bietet ein ausgefeiltes Fixture-System – vorausgesetzt, man versteht die Mechanismen dahinter.

12 Min. Lesezeit @magentoDataFixture · Rollback · Fixture-Klassen · Pfadauflösung Magento 2.4 · PHP 8.4 · PHPUnit 10

1. Was sind Magento Test Fixtures?

Ein Test-Fixture ist ein definierter, bekannter Datenbankzustand, der vor einem Test hergestellt wird, damit der Test unter kontrollierten Bedingungen läuft. In Magento bedeutet das konkret: Ein Fixture-Skript legt Produkte, Kategorien, Kundendaten oder Konfigurationswerte an, die der Test benötigt. Nach dem Test werden diese Daten entweder per Rollback-Skript explizit gelöscht oder – wenn @magentoDbIsolation enabled aktiv ist – per Transaktion zurückgerollt.

Der entscheidende Unterschied zu setUp()-Methoden: Fixtures können zwischen Tests geteilt werden. Wenn zehn Tests dasselbe Produkt benötigen, muss das Fixture nur einmal ausgeführt werden, wenn es auf Klassenebene annotiert ist. Das Magento Test Framework unterscheidet zwischen Fixture-Skripten (einfache PHP-Dateien, die direkt ausgeführt werden) und Fixture-Klassen (Implementierungen von DataFixtureInterface, die den DI-Container nutzen können). Ab Magento 2.4.4 sind Fixture-Klassen der empfohlene Ansatz, weil sie typsicher, testbar und wiederverwendbar sind.

2. Die @magentoDataFixture Annotation im Detail

Die Annotation @magentoDataFixture akzeptiert entweder einen Pfad zu einem PHP-Skript oder den vollqualifizierten Klassennamen einer Fixture-Klasse. Pfade werden relativ zum Magento-Root oder zur Testsuite-Basis aufgelöst – die genaue Auflösung ist einer der häufigsten Fehlerquellen. Wird die Annotation auf Klassenebene gesetzt, läuft das Fixture einmal für alle Testmethoden der Klasse. Auf Methodenebene läuft es einmal pro Testmethode. Beide Varianten können kombiniert werden: Ein Klassen-Fixture legt Basis-Daten an, ein Methoden-Fixture ergänzt methodenspezifische Daten.

Ab Magento 2.4.4 ist die bevorzugte Syntax für Fixture-Klassen die PHP 8 Attribut-Syntax: #[DataFixture(ProductFixture::class, ['sku' => 'test-001'], 'product')]. Das dritte Argument ist ein Alias, über den der Test auf das von der Fixture zurückgegebene Objekt zugreifen kann, ohne die Datenbank nochmals abfragen zu müssen. Diese Fixture-Daten werden dem Test über $this->fixtures->get('product') zugänglich gemacht – ein elegantes Muster, das die Tests lesbarer und schneller macht.

<?php

declare(strict_types=1);

namespace Mironsoft\Catalog\Test\Integration;

use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\TestFramework\Fixture\DataFixture;
use Magento\TestFramework\Fixture\DataFixtureStorage;
use Magento\TestFramework\Fixture\DataFixtureStorageManager;
use Magento\TestFramework\Helper\Bootstrap;
use PHPUnit\Framework\TestCase;
// Built-in Magento fixture classes
use Magento\Catalog\Test\Fixture\Product as ProductFixture;
use Magento\Catalog\Test\Fixture\Category as CategoryFixture;

/**
 * Tests using the modern DataFixture attribute syntax (Magento 2.4.4+).
 */
class ProductWithCategoryTest extends TestCase
{
    private DataFixtureStorage $fixtures;

    protected function setUp(): void
    {
        parent::setUp();
        $this->fixtures = Bootstrap::getObjectManager()
            ->get(DataFixtureStorageManager::class)
            ->getStorage();
    }

    /**
     * @test
     * @magentoDbIsolation enabled
     */
    #[DataFixture(CategoryFixture::class, ['name' => 'Test Category'], 'cat')]
    #[DataFixture(ProductFixture::class, ['sku' => 'fixture-test-001', 'category_ids' => ['$cat.id$']], 'product')]
    public function productIsAssignedToCategory(): void
    {
        // Retrieve fixture objects directly — no DB query needed
        $category = $this->fixtures->get('cat');
        $product  = $this->fixtures->get('product');

        $this->assertNotEmpty($category->getId());
        $this->assertContains(
            (int) $category->getId(),
            $product->getCategoryIds()
        );
    }
}

3. Ein Fixture-Skript schreiben

Ein klassisches Fixture-Skript ist eine einfache PHP-Datei ohne Namensraum, die direkt in der Testumgebung ausgeführt wird. Der Magento-DI-Container ist verfügbar über Bootstrap::getObjectManager(). Typischerweise legt das Skript Datensätze an und speichert relevante IDs in der Registry oder einer globalen Variable, damit der Test darauf zugreifen kann. Dieses Muster ist in vielen Magento-Core-Tests zu sehen und funktioniert zuverlässig, hat aber einen Nachteil: Die Kommunikation zwischen Fixture und Test läuft über den globalen Zustand.

Das Fixture-Skript liegt per Konvention im selben Verzeichnis wie die Testklasse, in einem Unterordner _files/. Alternativ kann der Pfad absolut oder relativ zum Testsuite-Root angegeben werden. Wichtig: Das Fixture-Skript muss nicht nur den Aufbau-Code enthalten – für explizite Rollbacks braucht man ein zweites Skript mit dem Suffix _rollback.php, das die vom Fixture angelegten Daten wieder löscht. Dieses Rollback-Skript wird vom Framework nach dem Test automatisch ausgeführt, wenn @magentoDbIsolation deaktiviert ist.

<?php
// dev/tests/integration/testsuite/Mironsoft/Catalog/Test/Integration/_files/simple_product.php
// Fixture script: creates a simple product for integration tests

use Magento\Catalog\Api\Data\ProductInterfaceFactory;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\TestFramework\Helper\Bootstrap;

$objectManager       = Bootstrap::getObjectManager();
$productFactory      = $objectManager->get(ProductInterfaceFactory::class);
$productRepository   = $objectManager->get(ProductRepositoryInterface::class);

$product = $productFactory->create();
$product
    ->setSku('mironsoft-fixture-product')
    ->setName('Fixture Test Product')
    ->setTypeId('simple')
    ->setAttributeSetId(4)
    ->setPrice(29.99)
    ->setStatus(1)
    ->setVisibility(4)
    ->setStockData(['qty' => 100, 'is_in_stock' => 1]);

$productRepository->save($product);

4. Rollback-Skripte: Warum und wie

Rollback-Skripte sind das explizite Gegenstück zu Fixture-Skripten und werden benötigt, wenn @magentoDbIsolation disabled ist. Das ist der Fall bei Tests, die explizit über Transaktionsgrenzen hinweg testen oder wenn Fixtures auf Klassenebene gesetzt sind, die nicht für jede Testmethode zurückgerollt werden sollen. Das Rollback-Skript trägt denselben Namen wie das Fixture-Skript mit dem Suffix _rollback.php und wird automatisch vom Framework aufgerufen.

Ein häufiger Fehler: Das Rollback-Skript versucht, Daten zu löschen, die durch den Transaktions-Rollback bereits zurückgerollt wurden – was einen Fehler erzeugt, weil die Entität nicht mehr existiert. Die Lösung ist eine defensive Prüfung: Repository-Operationen in einen Try-Catch-Block einschließen oder explizit prüfen, ob die Entität noch existiert, bevor sie gelöscht wird. Rollback-Skripte sollten idempotent sein – mehrfach ausgeführt darf kein Fehler entstehen.

<?php
// _files/simple_product_rollback.php
// Rollback script: removes the fixture product created by simple_product.php

use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Registry;
use Magento\TestFramework\Helper\Bootstrap;

$objectManager     = Bootstrap::getObjectManager();
$registry          = $objectManager->get(Registry::class);
$productRepository = $objectManager->get(ProductRepositoryInterface::class);

// Disable security check for deletion in test context
$registry->unregister('isSecureArea');
$registry->register('isSecureArea', true);

try {
    // Idempotent: no error if product was already removed by DB rollback
    $productRepository->deleteById('mironsoft-fixture-product');
} catch (NoSuchEntityException) {
    // Product already gone — rollback ran or fixture never created it
}

$registry->unregister('isSecureArea');
$registry->register('isSecureArea', false);

5. Fixture-Klassen mit DataFixtureInterface

Fixture-Klassen implementieren das Interface Magento\TestFramework\Fixture\DataFixtureInterface und haben gegenüber Fixture-Skripten mehrere Vorteile: Sie können über Constructor Injection Dependencies erhalten, sind typsicher, können Daten zurückgeben (für den Fixtures-Alias-Mechanismus) und sind wiederverwendbar über mehrere Testklassen hinweg. Magento 2.4.4 hat für alle Core-Entitäten eingebaute Fixture-Klassen eingeführt – Produkte, Kategorien, Kunden, CMS-Pages – die direkt mit Parametern genutzt werden können.

Eigene Fixture-Klassen schreibt man für domänenspezifische Testdaten. Die apply()-Methode erhält ein Array mit Parametern, legt die Entität an und gibt ein DataObject zurück, das der Alias im Test referenzieren kann. Das Framework steuert den Lifecycle: apply() wird vor dem Test aufgerufen, das Framework kümmert sich um das Aufräumen basierend auf der DB-Isolation. Keine separaten Rollback-Skripte notwendig – wenn @magentoDbIsolation enabled aktiv ist, erledigt die Transaktion das Aufräumen.

6. Pfadauflösung und Fixture-Lokalisierung

Die Pfadauflösung bei @magentoDataFixture folgt einer bestimmten Suchreihenfolge, die man kennen muss. Bei einem Pfad wie Mironsoft/Catalog/Test/Integration/_files/simple_product.php sucht das Framework im Testsuite-Root-Verzeichnis, also unter dev/tests/integration/testsuite/. Bei einem absoluten Pfad mit ::class-Syntax wird die Fixture-Klasse über den Autoloader geladen. Ein Pfad wie ../../app/code/Mironsoft/Catalog/Test/Integration/_files/simple_product.php ist relativ zum Bootstrap-Verzeichnis – eine Quelle häufiger Verwirrung.

Die empfohlene Struktur legt Fixture-Skripte immer unter dem selben Verzeichnis wie die Testklasse ab: dev/tests/integration/testsuite/Mironsoft/Catalog/Test/Integration/_files/. Das macht Pfade kürzer, Fixture-Skripte leichter auffindbar und vermeidet Mehrdeutigkeiten. Bei der Verwendung von Fixture-Klassen (moderne Syntax) entfällt das Problem komplett, da Klassen über den Standard-Autoloader aufgelöst werden.

7. Fixture-Abhängigkeiten und Ausführungsreihenfolge

Wenn ein Fixture ein anderes Fixture als Voraussetzung hat – etwa ein Produkt braucht eine Kategorie, die braucht einen Root-Catalog-Node – muss die Ausführungsreihenfolge stimmen. Bei der Skript-Annotation werden Fixtures in der Reihenfolge ihrer Annotation-Deklaration ausgeführt. Bei der Attribut-Syntax gilt dieselbe Regel. Das Framework bietet keinen expliziten Dependency-Mechanismus zwischen Fixtures – Abhängigkeiten müssen durch Reihenfolge oder durch ein zusammengesetztes Fixture gelöst werden.

Ein saubereres Muster für komplexe Fixture-Abhängigkeiten: Ein dediziertes Fixture-Skript oder eine Fixture-Klasse, die alle nötigen Voraussetzungen in der richtigen Reihenfolge anlegt. Das verhindert subtile Fehler durch Annotationsreihenfolge und macht die Fixture-Logik an einer Stelle zentralisiert. Bei der Alias-Syntax kann ein Fixture den Output eines vorherigen Fixtures referenzieren: ['category_id' => '$cat.id$'] referenziert das id-Feld des Fixtures mit dem Alias cat.

8. Häufige Fallstricke und deren Behebung

Fallstrick 1: Fixture legt Daten an, aber der Test findet sie nicht. Ursache ist fast immer ein Caching-Problem: Magento cached Repository-Ergebnisse im Request-Kontext. Ein neu angelegtes Produkt ist im Cache des Repositories möglicherweise nicht sichtbar, wenn es zuvor für eine andere Entität (z.B. leeres Ergebnis) gecacht wurde. Lösung: Cache des Repositories explizit invalidieren oder eine frische Repository-Instanz anfordern.

Fallstrick 2: Tests laufen einzeln, schlagen aber in der Suite fehl. Das deutet auf fehlende Isolation hin – ein vorheriger Test hat Daten oder Zustand hinterlassen, der diesen Test stört. Lösung: @magentoDbIsolation enabled sicherstellen und prüfen, ob der Test auf externe Dienste (Elasticsearch, Redis) zugreift, die nicht per Transaktion zurückgerollt werden. Fallstrick 3: Rollback-Skript schlägt fehl mit "Entity not found". Rollback-Skript muss defensiv sein – immer try-catch um Delete-Operationen.

Fixture-Typ Syntax Rollback Empfehlung
Skript-Fixture @magentoDataFixture path/to/file.php Manuell via _rollback.php Legacy-Tests, einfache Szenarien
Fixture-Klasse alt @magentoDataFixture Vendor\...\Fixture Automatisch via DB-Isolation Magento 2.4.x ohne PHP 8 Attribute
Fixture-Klasse neu #[DataFixture(Class::class, [...], 'alias')] Automatisch via DB-Isolation Empfohlen ab Magento 2.4.4
setUp()-Methode PHP-Code in setUp() Manuell in tearDown() Nur wenn keine Fixture-Alternative
Core Fixtures Magento\Catalog\Test\Fixture\Product Automatisch Immer zuerst Core-Fixtures prüfen

9. Fixture Best Practices zusammengefasst

Gute Fixtures folgen dem Minimalprinzip: Sie legen nur die Daten an, die der Test wirklich benötigt. Ein Fixture, das ein vollständiges Produkt mit allen Attributen, Lagerbestand, Preisregeln und Kategorieverknüpfungen anlegt, wenn der Test nur die SKU prüft, verlangsamt den Test unnötig und macht ihn fragil gegenüber Schema-Änderungen. Minimal nötige Fixtures sind schneller, klarer in ihrer Absicht und leichter zu warten.

Mironsoft

Magento 2 Testing, Fixtures und Qualitätssicherung

Fragile Fixtures durch robuste Testdaten-Verwaltung ersetzen?

Wir migrieren eure bestehenden Fixture-Skripte auf moderne DataFixture-Klassen, lösen Isolationsprobleme und bauen eine wartbare Fixture-Bibliothek für euer Magento-Projekt auf.

Fixture-Migration

Skript-Fixtures auf typsichere DataFixture-Klassen migrieren

Isolationsanalyse

Nicht-deterministische Tests durch Isolationsprobleme finden und beheben

Fixture-Bibliothek

Wiederverwendbare Fixture-Klassen für euer Domain-Modell entwickeln

10. Zusammenfassung

Magento Test Fixtures sind das Fundament deterministischer Integrationstests. Die Entwicklung geht klar in Richtung Fixture-Klassen mit der PHP 8 Attribut-Syntax: typsicher, wiederverwendbar, ohne Pfadauflösungsprobleme und mit dem eleganten Alias-Mechanismus für den Zugriff auf Fixture-Daten im Test. Für neue Tests sollte man immer zuerst prüfen, ob Magento Core Fixture-Klassen für die benötigten Entitäten existieren (Magento\Catalog\Test\Fixture\Product, Category, Customer etc.) – das spart erheblich Implementierungsaufwand.

Die kritische Eigenschaft guter Fixtures ist Minimalität und Isolation: Nur anlegen, was der Test wirklich braucht, und sicherstellen, dass Fixtures nach dem Test vollständig aufgeräumt werden. @magentoDbIsolation enabled ist dafür die einfachste Lösung – Fixtures in einer Transaktion, Rollback am Testende. Wer Fixtures auf Klassenebene setzt und DB-Isolation deaktiviert, braucht explizite Rollback-Skripte, die defensiv und idempotent sein müssen.

Magento Test Fixtures — Das Wichtigste auf einen Blick

Moderne Syntax

#[DataFixture(Product::class, ['sku' => 'x'], 'alias')] – typsicher, wiederverwendbar, kein Pfadproblem. Ab Magento 2.4.4 bevorzugen.

Rollback-Strategie

Mit @magentoDbIsolation enabled übernimmt die Transaktion das Aufräumen. Ohne Isolation braucht jedes Fixture ein _rollback.php-Pendant.

Core Fixtures nutzen

Magento liefert Fixture-Klassen für alle Standard-Entitäten. Immer zuerst Magento\Catalog\Test\Fixture\ prüfen, bevor eigene Skripte geschrieben werden.

Minimalitätsprinzip

Fixture legt nur Daten an, die der Test wirklich braucht. Überschüssige Fixture-Daten verlangsamen Tests und erhöhen die Kopplung an das Datenbankschema.

11. FAQ: Magento Test Fixtures

1Unterschied @magentoDataFixture vs. setUp()?
@magentoDataFixture wird vom Framework verwaltet, kann auf Klassen- oder Methodenebene geteilt werden. setUp() läuft immer pro Methode ohne Framework-Verwaltung.
2Wann brauche ich ein Rollback-Skript?
Wenn @magentoDbIsolation disabled ist. Mit DB-Isolation übernimmt die Transaktion das Aufräumen – Rollback-Skript nicht nötig.
3Wie greife ich auf Fixture-Daten zu?
Mit Alias: #[DataFixture(Product::class, [...], 'prod')] dann $this->fixtures->get('prod') im Test. Kein DB-Re-Query nötig.
4Kann ich dasselbe Fixture mehrfach mit anderen Parametern nutzen?
Ja – mehrere DataFixture-Attribute mit unterschiedlichen Parametern und Aliases erzeugen separate Entitäten.
5Wo lege ich eigene Fixture-Klassen ab?
app/code/Mironsoft/Module/Test/Fixture/ oder dev/tests/integration/testsuite/Mironsoft/Module/Fixture/. Autoloader findet sie – kein Pfadproblem.
6Kann ich Magento Core Fixtures nutzen?
Ja – Magento\Catalog\Test\Fixture\Product, Category, Customer etc. sind für eigene Tests konzipiert. Immer zuerst Core-Fixtures prüfen.
7Klassenebene-Fixture mit Methoden-DB-Isolation?
Vorsicht: Methoden-Isolation rollt auch Klassen-Fixtures zurück. Kombination sorgfältig planen – Klassen-Fixtures nur mit Klassen-Isolation kombinieren.
8Fixtures mit Elasticsearch-Abhängigkeit?
Elasticsearch wird nicht per Transaktion zurückgerollt. tearDown() muss Index explizit aufräumen oder zurücksetzen.
9Was gibt apply() in einer Fixture-Klasse zurück?
Ein DataObject. Felder sind über $alias.field$ in nachfolgenden Fixtures referenzierbar und per $this->fixtures->get('alias') im Test abrufbar.
10Fixture-Skript nicht gefunden?
Framework sucht relativ zu dev/tests/integration/testsuite/. Vollständigen Pfad ab dort angeben. __DIR__-basierte absolute Pfade sind zuverlässiger.