Playwright Fixtures für saubere Testisolation
PASS
expect()
Testing · Playwright · Fixtures · E2E
Playwright Fixtures für saubere Testisolation
Setup und Teardown ohne gemeinsamen Zustand

Playwright Fixtures ersetzen fragile beforeEach Blöcke und globalen Testzustand durch klar definierte, wiederverwendbare Bausteine für Setup und Teardown. Wer eingebaute Fixtures wie page, context und browser versteht und eigene Fixtures mit test.extend erstellt, erhält Tests, die parallel laufen, sich niemals gegenseitig beeinflussen und trotzdem kaum Code duplizieren. So bleibt jede Testdatei unabhängig lesbar und wartbar.

16 Min. Lesezeit test.extend · Worker Scope · Teardown Playwright · TypeScript · CI/CD

1. Warum Fixtures das Problem gemeinsamen Zustands lösen

Die klassische Testarchitektur mit beforeEach und modulweiten Variablen wirkt zunächst harmlos, führt aber in wachsenden Testsuiten zu gemeinsamem, veränderlichem Zustand. Ein Login-Objekt, das in einem übergeordneten describe-Block einmalig erzeugt und danach in jedem Test wiederverwendet wird, überträgt seinen Zustand unbeabsichtigt von einem Test zum nächsten. Läuft ein Test parallel oder in anderer Reihenfolge, entstehen genau die schwer reproduzierbaren, flackernden Fehler, die E2E-Testsuiten in der Praxis unbrauchbar machen.

Playwright Fixtures lösen dieses Problem strukturell, nicht durch Disziplin. Jede Fixture wird für jeden Test, der sie anfordert, frisch instanziiert und am Ende automatisch wieder aufgeräumt, ohne dass eine Testdatei explizit daran denken muss. Der entscheidende Unterschied zu beforeEach: Fixtures werden per Dependency Injection über die Parameter der Testfunktion angefordert, nicht global registriert. Ein Test, der eine Fixture nicht braucht, instanziiert sie auch nicht, was Testläufe zusätzlich beschleunigt und die Kopplung zwischen Tests sichtbar reduziert.

2. Eingebaute Fixtures: page, context, browser und mehr

Playwright liefert bereits eine Reihe eingebauter Fixtures mit, die den Grundstein jeder Testdatei bilden. Die Fixture page stellt eine frische Seite in einem isolierten Browser-Kontext bereit, context gibt Zugriff auf den zugrunde liegenden BrowserContext für Cookies, Storage oder mehrere Tabs, und browser liefert die gemeinsam genutzte Browser-Instanz für die gesamte Worker-Ausführung. Zusätzlich existieren browserName, request für reine API-Aufrufe ohne Browser sowie Konfigurationswerte wie baseURL, die aus playwright.config.ts gespeist werden.

Wichtig ist die Isolationsgarantie hinter context: Jeder Test erhält standardmäßig einen komplett neuen Browser-Kontext mit leerem Cookie-Speicher und leerem Local Storage, selbst wenn dieselbe browser-Instanz wiederverwendet wird. Das ist der Grund, warum zwei parallele Tests sich niemals gegenseitig einloggen oder ausloggen können, obwohl beide denselben Browser-Prozess teilen. Projekt-Konfiguration wie storageState lässt sich pro Projekt in der Config setzen und wird dann automatisch in jede context-Fixture übernommen.


// playwright.config.ts: Basis-Konfiguration fuer eingebaute Fixtures
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  workers: process.env.CI ? 4 : undefined,
  use: {
    // Wird automatisch in die "page" Fixture jedes Tests uebernommen
    baseURL: 'https://staging.mironsoft-shop.test',
    trace: 'retain-on-failure',
  },
  projects: [
    {
      name: 'guest',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'logged-in',
      use: {
        ...devices['Desktop Chrome'],
        // storageState fuellt context/page bereits eingeloggt
        storageState: 'playwright/.auth/customer.json',
      },
    },
  ],
});

3. Eigene Fixtures mit test.extend() definieren

Eigene Fixtures entstehen über test.extend(), das ein neues, erweitertes test-Objekt zurückgibt. Jede Fixture wird als Funktion definiert, die zwei Parameter erhält: die bereits verfügbaren Fixtures und eine use-Funktion, an die der eigentliche Fixture-Wert übergeben wird. Dieses Muster wirkt zunächst ungewöhnlich, ermöglicht aber sowohl Setup vor use() als auch Teardown nach use() in einer einzigen, linearen Funktion, ohne separate Hook-Methoden.

Für Magento- und Hyvä-Shops lohnt sich eine kleine Bibliothek domänenspezifischer Fixtures: eine storefrontPage-Fixture, die bereits auf die Startseite navigiert ist, oder eine productPage-Fixture, die ein bekanntes Testprodukt öffnet. Der Vorteil gegenüber Page-Object-Konstruktoren in jedem Test: Die Fixture wird deklarativ als Parameter angefordert, TypeScript prüft die Typen automatisch, und Playwright kümmert sich um Instanziierung und Aufräumen.


// fixtures.ts: eigene Fixture per test.extend() definieren
import { test as base, expect } from '@playwright/test';
import { ProductPage } from './pages/ProductPage';

type MyFixtures = {
  productPage: ProductPage;
};

export const test = base.extend<MyFixtures>({
  productPage: async ({ page }, use) => {
    // Setup: Seite instanziieren und zu einem bekannten Produkt navigieren
    const productPage = new ProductPage(page);
    await productPage.goto('/testprodukt-blau.html');

    // Fixture-Wert an den Test uebergeben
    await use(productPage);

    // Teardown laeuft automatisch nach jedem Test, hier kein Aufwand noetig
  },
});

export { expect };

4. Das authenticated-page Pattern für Login-Zustand

Login-Formulare in jedem einzelnen Test manuell auszufüllen ist einer der häufigsten Gründe für langsame und instabile Testsuiten. Das authenticated-page Pattern löst dieses Problem, indem der Login-Vorgang genau einmal ausgeführt und der resultierende Zustand als storageState-Datei gespeichert wird. Eine authenticatedPage-Fixture lädt diesen gespeicherten Zustand anschließend in jeden Test, der sie anfordert, ohne dass die UI-Login-Schritte erneut durchlaufen werden müssen.

Entscheidend für saubere Isolation ist, dass jeder Test trotz geteiltem Login-Zustand einen eigenen Browser-Kontext erhält. Ändert ein Test etwas am Warenkorb oder an Adressdaten, bleibt das für parallele Tests unsichtbar, weil storageState beim Laden kopiert, nicht referenziert wird. Für Tests, die einen komplett frischen Account benötigen, etwa Registrierungs-Flows, lässt sich die Fixture zusätzlich parametrisieren und erzeugt dann bei Bedarf einen neuen Testkunden über die Magento-REST-API statt über die eingeloggte Fixture.

5. Worker-scoped vs. test-scoped Fixtures

Playwright unterscheidet zwischen test-scoped Fixtures, die für jeden einzelnen Test neu erzeugt werden, und worker-scoped Fixtures, die genau einmal pro Test-Worker instanziiert und über alle Tests dieses Workers hinweg wiederverwendet werden. Der Scope wird über die Option { scope: 'worker' } im Fixture-Definitionsobjekt gesteuert, Standardwert ist immer test. Worker-scoped Fixtures eignen sich für teure, aber zustandslos nutzbare Ressourcen: eine Datenbankverbindung, ein API-Client mit Basis-Token oder ein einmalig gesätes Test-Datenset.

Die Faustregel: Sobald ein Test etwas an der Fixture verändert, das ein anderer Test sehen könnte, gehört sie zwingend auf test-Scope. Nur rein lesende oder wirklich unveränderliche Ressourcen dürfen worker-scoped sein. Ein häufiger Fehler ist das Seeden einer Produktvariante als worker-scoped Fixture, die anschließend in einem Test gelöscht wird, worauf alle nachfolgenden Tests desselben Workers fehlschlagen, weil die Ressource plötzlich fehlt.


// fixtures.ts: worker-scoped Fixture fuer teures, wiederverwendbares Setup
import { test as base } from '@playwright/test';
import { seedTestCatalog, TestCatalog } from './helpers/catalog';

type WorkerFixtures = {
  testCatalog: TestCatalog;
};

export const test = base.extend<{}, WorkerFixtures>({
  testCatalog: [async ({}, use) => {
    // Nur einmal pro Worker ausgefuehrt, nicht pro Test
    const catalog = await seedTestCatalog({ productCount: 20 });

    await use(catalog);

    // Teardown laeuft einmal, wenn der Worker beendet wird
    await catalog.cleanup();
  }, { scope: 'worker' }],
});

6. Fixture-Komposition und Abhängigkeitsketten

Fixtures können auf anderen Fixtures aufbauen, indem sie diese einfach im ersten Funktionsparameter anfordern. Eine checkoutPage-Fixture kann so auf einer bereits vorbereiteten authenticatedPage-Fixture aufsetzen, die wiederum die eingebaute page-Fixture nutzt. Playwright löst diese Abhängigkeitskette automatisch auf und instanziiert nur die Fixtures, die für den jeweiligen Test tatsächlich benötigt werden, in der korrekten Reihenfolge.

Diese Komposition ersetzt tief verschachtelte Hilfsfunktionen und Setup-Wrapper vollständig. Statt einer Funktion setupCheckoutWithLogin(page), die intern mehrere andere Setup-Funktionen aufruft, bekommt jeder Test einfach die fertige checkoutPage-Fixture als Parameter. Der zusätzliche Vorteil: Jede Fixture in der Kette bleibt einzeln testbar und wiederverwendbar, weil sie nicht weiß, wer sie letztlich konsumiert. Neue Fixtures lassen sich so über die Zeit zu einer wachsenden, gut typisierten Bibliothek zusammensetzen.


// fixtures.ts: Fixture-Kette ueber mehrere Abhaengigkeitsebenen
import { test as base } from '@playwright/test';
import { CheckoutPage } from './pages/CheckoutPage';

type ChainedFixtures = {
  authenticatedPage: import('@playwright/test').Page;
  checkoutPage: CheckoutPage;
};

export const test = base.extend<ChainedFixtures>({
  authenticatedPage: async ({ browser }, use) => {
    const context = await browser.newContext({
      storageState: 'playwright/.auth/customer.json',
    });
    const page = await context.newPage();
    await use(page);
    await context.close();
  },

  // Haengt direkt von der authenticatedPage Fixture ab
  checkoutPage: async ({ authenticatedPage }, use) => {
    const checkout = new CheckoutPage(authenticatedPage);
    await checkout.addTestProductToCart();
    await checkout.goto();

    await use(checkout);
  },
});

7. Teardown-Patterns: Aufräumen mit yield

Das Muster hinter Playwright-Fixtures folgt dem gleichen Prinzip wie ein Python-Context-Manager: Alles vor await use(value) ist Setup, alles danach ist Teardown. Entscheidend ist, dass der Code nach use() auch dann ausgeführt wird, wenn der Test fehlschlägt oder eine Assertion wirft, solange die Fixture selbst erfolgreich initialisiert wurde. Das ersetzt afterEach vollständig, weil Teardown-Logik dort lebt, wo auch das zugehörige Setup steht, statt in einer separat gepflegten Funktion.

Für Ressourcen, die auch bei einem Fehler im Setup selbst sauber freigegeben werden müssen, gehört der kritische Teil in einen try/finally-Block innerhalb der Fixture. Typische Teardown-Aufgaben in Magento-Kontext: einen über die REST-API angelegten Testkunden wieder löschen, eine per page.context().tracing gestartete Aufzeichnung stoppen, oder eine per Fixture geöffnete Datenbankverbindung schließen. Weil dieser Code garantiert ausgeführt wird, sammeln sich über eine Testsuite hinweg keine verwaisten Testdaten mehr an.


// fixtures.ts: garantiertes Teardown per yield-Pattern (use als Uebergabepunkt)
import { test as base } from '@playwright/test';
import { createTestCustomer, deleteTestCustomer } from './helpers/api';

type CustomerFixture = {
  testCustomer: { id: number; email: string };
};

export const test = base.extend<CustomerFixture>({
  testCustomer: async ({ request }, use) => {
    // Setup vor use(): Testkunden ueber die REST-API anlegen
    const customer = await createTestCustomer(request);

    try {
      // Uebergabe an den Test, Ausfuehrung pausiert hier bis Testende
      await use(customer);
    } finally {
      // Laeuft garantiert, auch wenn der Test fehlschlaegt
      await deleteTestCustomer(request, customer.id);
    }
  },
});

8. Häufige Fehler und Anti-Patterns bei Fixtures

Der häufigste Fehler ist, teure Setup-Logik als test-scoped Fixture zu belassen, obwohl sie eigentlich unveränderlich ist und problemlos worker-scoped sein könnte. Das kostet in großen Suiten spürbar Laufzeit, weil dieselbe Arbeit hundertfach wiederholt wird. Der umgekehrte Fehler ist mindestens genauso gefährlich: veränderlichen Zustand fälschlich worker-scoped zu machen und damit genau die Kopplung zwischen Tests wieder einzuführen, die Fixtures eigentlich verhindern sollen.

Ein weiteres verbreitetes Problem sind Fixtures mit Seiteneffekten außerhalb ihres eigenen Verantwortungsbereichs, etwa eine page-Fixture, die zusätzlich globale Test-Reporter-Zustände verändert. Auch auto: true Fixtures, die ohne explizite Anforderung bei jedem Test laufen, sollten sparsam eingesetzt werden, weil sie unsichtbares Verhalten erzeugen, das in der Testdatei selbst nicht mehr erkennbar ist. Wer eine Fixture öffnet und dabei vergisst, sie über try/finally abzusichern, riskiert zudem verwaiste Ressourcen bei fehlschlagenden Tests.

9. Fixture-Patterns im direkten Vergleich

Die folgende Übersicht stellt klassische Testmuster den entsprechenden Playwright-Fixture-Patterns gegenüber und zeigt, warum die Fixture-Variante in nahezu jedem Fall die robustere Wahl ist.

Bereich Anti-Pattern Empfohlenes Fixture-Pattern Vorteil
Zustand pro Test beforeEach mit gemeinsamem, veraenderlichem Zustand test-scoped Fixture Keine Test-zu-Test-Kopplung mehr moeglich
Teures Setup Globales Setup-Skript vor der ganzen Suite worker-scoped Fixture Einmal pro Worker statt einmal global
Login-Zustand Manueller UI-Login in jedem einzelnen Test authenticated-page Fixture Login einmal, storageState wiederverwendet
Aufraeumen afterEach-Cleanup getrennt vom Setup Fixture-Teardown via yield/use() Garantiert auch bei fehlschlagenden Tests
Wiederverwendung Duplizierter Setup-Code in jeder Testdatei Fixture-Komposition Zentral gepflegt, einzeln testbar

In der Praxis lassen sich fast alle Stabilitätsprobleme großer E2E-Suiten auf eine der fünf Zeilen in dieser Tabelle zurückführen. Wer konsequent auf Fixtures statt auf Hooks setzt, gewinnt nicht nur Lesbarkeit, sondern vor allem die Möglichkeit, Tests ohne Angst vor Seiteneffekten parallel auszuführen.

Mironsoft

E2E-Testautomatisierung mit Playwright für Magento- und Hyvä-Shops

Stabile Playwright-Testsuite aufbauen?

Wir bauen für euren Magento- oder Hyvä-Shop eine wartbare Playwright-Fixture-Bibliothek: von der authenticated-page Fixture bis zur CI-Pipeline, die parallel und ohne Flakiness läuft.

Fixture-Architektur

test.extend-Bibliothek mit klaren Scope-Grenzen und Teardown

Testisolation-Audit

Bestehende Suites auf gemeinsamen Zustand und Flakiness prüfen

CI/CD-Integration

Parallele Worker, Sharding und Reports in eurer Pipeline

10. Zusammenfassung

Playwright Fixtures lösen das Kernproblem klassischer E2E-Testsuiten: gemeinsamen, veränderlichen Zustand zwischen Tests. Eingebaute Fixtures wie page, context und browser liefern isolierte Grundlagen für jeden Test, während eigene Fixtures über test.extend() domänenspezifisches Setup wie das authenticated-page Pattern deklarativ verfügbar machen. Die Wahl zwischen test-scoped und worker-scoped entscheidet dabei direkt über Laufzeit und Sicherheit: veränderlicher Zustand gehört auf Test-Scope, teure aber unveränderliche Ressourcen dürfen worker-scoped sein.

Fixture-Komposition ersetzt verschachtelte Setup-Funktionen durch klar typisierte, einzeln testbare Bausteine, und das yield-Pattern mit use() garantiert Teardown selbst bei fehlschlagenden Tests. Wer diese Prinzipien konsequent anwendet, erhält eine Testsuite, die parallel läuft, ohne dass ein Test die Ergebnisse eines anderen beeinflusst, und die mit jeder neuen Fixture wartbarer statt komplexer wird.

Playwright Fixtures - Das Wichtigste auf einen Blick

Eingebaute Fixtures

page, context, browser und request liefern isolierte Basis-Ressourcen pro Test.

test.extend()

Eigene Fixtures deklarativ definieren, per Dependency Injection im Test anfordern.

Scope-Wahl

Veraenderlicher Zustand test-scoped, teure unveraenderliche Ressourcen worker-scoped.

Teardown

Code nach use() in try/finally laeuft garantiert, auch bei Testfehlern.

11. FAQ: Playwright Fixtures

1Was ist eine Fixture in Playwright genau?
Ein wiederverwendbarer Baustein fuer Setup und Teardown, angefordert per Dependency Injection ueber die Testparameter. Playwright instanziiert sie nur bei Bedarf und raeumt automatisch auf.
2Wie unterscheiden sich test-scoped und worker-scoped Fixtures?
Test-scoped wird pro Test neu erzeugt, worker-scoped einmal pro Worker und dann wiederverwendet. Veraenderlicher Zustand gehoert auf Test-Scope, teure unveraenderliche Ressourcen duerfen worker-scoped sein.
3Wie erstelle ich eine eigene Fixture mit test.extend()?
Mit base.extend() ein Objekt mit Fixture-Funktionen uebergeben. Jede Funktion bekommt bestehende Fixtures und eine use()-Funktion; Code davor ist Setup, Code danach Teardown.
4Wie funktioniert die authenticated-page Fixture fuer Login-Status?
Login laeuft einmal, das Ergebnis wird als storageState gespeichert. Eine authenticatedPage-Fixture laedt diesen Zustand pro Test, UI-Login-Schritte entfallen dadurch.
5Wie funktioniert Teardown mit dem yield-Pattern in einer Fixture?
Alles nach await use(value) ist Teardown und laeuft garantiert, auch bei Testfehlern. Kritische Freigaben zusaetzlich in try/finally kapseln.
6Kann eine Fixture von einer anderen Fixture abhaengen?
Ja, ueber den ersten Funktionsparameter, etwa checkoutPage basierend auf authenticatedPage. Playwright loest die Abhaengigkeitskette automatisch in korrekter Reihenfolge auf.
7Was ist eine automatische Fixture (auto: true)?
Laeuft bei jedem Test automatisch ohne explizite Anforderung, etwa fuer globales Tracing. Sparsam einsetzen, da unsichtbares Verhalten in der Testdatei nicht erkennbar ist.
8Warum sollte ich beforeEach/afterEach durch Fixtures ersetzen?
beforeEach/afterEach laufen fuer jeden Test unabhaengig vom Bedarf und foerdern gemeinsamen Zustand. Fixtures werden nur bei Bedarf instanziiert und halten Setup/Teardown zusammen.
9Wie teile ich storageState zwischen Tests, ohne Zustand zu teilen?
storageState wird beim Erzeugen eines neuen Kontexts kopiert, nicht referenziert. Jeder Test startet gleich eingeloggt, Aenderungen wirken sich nie auf parallele Tests aus.
10Was sind haeufige Fehler beim Einsatz von Fixtures?
Teures Setup faelschlich test-scoped, veraenderlichen Zustand faelschlich worker-scoped, und fehlendes try/finally beim Teardown mit verwaisten Testdaten als Folge.