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.
Inhaltsverzeichnis
- 1. Warum Fixtures das Problem gemeinsamen Zustands lösen
- 2. Eingebaute Fixtures: page, context, browser und mehr
- 3. Eigene Fixtures mit test.extend() definieren
- 4. Das authenticated-page Pattern für Login-Zustand
- 5. Worker-scoped vs. test-scoped Fixtures
- 6. Fixture-Komposition und Abhängigkeitsketten
- 7. Teardown-Patterns: Aufräumen mit yield
- 8. Häufige Fehler und Anti-Patterns bei Fixtures
- 9. Fixture-Patterns im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
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.