Warum Shared State der häufigste Grund für Flaky Tests ist
Tests, die einzeln zuverlässig bestehen, aber in der vollen Suite oder bei paralleler Ausführung plötzlich fehlschlagen, deuten fast immer auf geteilten Zustand zwischen Browser Storage, Datenbank und Testdaten hin. Dieser Artikel zeigt konkrete Isolationsstrategien für Cypress und Playwright, von eindeutigen Testdaten pro Worker bis zu sauberem Cleanup, damit E2E Suiten stabil, parallel sicher und reproduzierbar bleiben.
Inhaltsverzeichnis
- 1. Warum Tests einzeln bestehen, aber in der Suite scheitern
- 2. Browser-Storage isolieren: localStorage, sessionStorage, Cookies
- 3. Datenbank- und Backend-Zustand pro Test isolieren
- 4. Reihenfolgenunabhängige Tests schreiben
- 5. Parallel-sichere Testdaten: eindeutige Werte pro Worker
- 6. Cypress- und Playwright-spezifische Isolationsmechanismen
- 7. Teardown- und Cleanup-Strategien für Testdaten
- 8. Flaky Tests diagnostizieren: Shared State als Ursache
- 9. Isolationsmuster im Vergleich: schlecht vs. gut
- 10. Zusammenfassung
- 11. FAQ
1. Warum Tests einzeln bestehen, aber in der Suite scheitern
Tests, die isoliert per npx playwright test login.spec.ts grün laufen, aber in der vollen Suite fehlschlagen, weisen fast immer auf geteilten Zustand hin. Der Testrunner führt dieselbe Umgebung, denselben Browser-Kontext oder dieselbe Datenbank für mehrere Tests hintereinander aus, und ein früherer Test hinterlässt Spuren, auf die ein späterer Test unbewusst angewiesen ist oder die ihn stören. Klassische Symptome: Ein Login-Test schlägt fehl, weil ein vorheriger Test bereits eine Session-Cookie gesetzt hat, oder ein Checkout-Test findet ein Produkt nicht mehr, weil ein anderer Test es zuvor gelöscht hat.
Der Reflex, bei solchen Fehlern einfach cy.wait() einzufügen oder Retries zu erhöhen, behebt nur das Symptom, nicht die Ursache. Isolation bedeutet, dass jeder Test seine eigene Ausgangslage herstellt, unabhängig davon, welche Tests vorher liefen und in welcher Reihenfolge. Das ist keine Nebensächlichkeit, sondern die Grundvoraussetzung für parallele Ausführung, verlässliche CI-Pipelines und Tests, die tatsächlich einen Fehler im Produktcode anzeigen, statt nur einen Fehler im Testaufbau.
2. Browser-Storage isolieren: localStorage, sessionStorage, Cookies
Playwright und Cypress starten bei jedem Test grundsätzlich mit leerem Zustand, doch sobald Tests explizit page.context() wiederverwenden oder cy.session() unsauber konfiguriert wird, überleben localStorage-Einträge, sessionStorage und Cookies zwischen Tests. Ein Warenkorb-Token im localStorage, das ein vorheriger Test gesetzt hat, führt dazu, dass ein nachfolgender Test plötzlich einen gefüllten statt leeren Warenkorb vorfindet, obwohl der Testcode das nirgends erwartet.
Playwright isoliert Storage sauber über eigene BrowserContext-Instanzen pro Test, Cypress benötigt dafür explizites cy.clearCookies(), cy.clearLocalStorage() und seit Version 12 cy.session() mit korrekt gesetzter cacheAcrossSpecs-Option. Wichtig ist, auch IndexedDB und Service-Worker-Caches im Blick zu behalten, da Standard-Cleanup-Befehle diese oft nicht erfassen und veraltete gecachte Antworten sonst reale Bugs verschleiern.
// Playwright: isolierter BrowserContext pro Test, Storage wird nie geteilt
import { test, expect } from '@playwright/test';
test.describe('Warenkorb', () => {
test('startet mit leerem Warenkorb', async ({ browser }) => {
// Neuer Context erzeugt frischen Storage, Cookies und Cache
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('/checkout/cart');
const cartItemCount = await page.locator('[data-testid="cart-item"]').count();
expect(cartItemCount).toBe(0);
await context.close();
});
});
// Cypress: Storage vor jedem Test explizit zuruecksetzen
beforeEach(() => {
cy.clearCookies();
cy.clearLocalStorage();
cy.window().then((win) => win.sessionStorage.clear());
});
3. Datenbank- und Backend-Zustand pro Test isolieren
Browser-seitige Isolation reicht nicht, wenn der Test gegen eine echte Magento-Instanz mit MySQL-Datenbank läuft. Ein E2E-Test, der eine Bestellung anlegt, verändert reale Datenbank-Zeilen, und ohne Rückbau sammeln sich über hunderte Testläufe verwaiste Kunden, Bestellungen und Produkte an, die spätere Tests verfälschen, etwa wenn ein Test die Anzahl der Bestellungen eines Kunden prüft.
Bewährte Strategien sind Datenbank-Transaktionen, die nach jedem Test zurückgerollt werden, dedizierte Seed-Skripte, die vor jedem Testlauf einen definierten Ausgangszustand herstellen, und Fixtures, die über die API statt direkt in der Datenbank angelegt werden, um näher an echtem Nutzerverhalten zu bleiben. Für Magento-Projekte bewährt sich ein Befehl, der vor der Suite einen Datenbank-Snapshot lädt, kombiniert mit gezieltem Cleanup einzelner Entitäten nach jedem Test statt eines kompletten Reset, der bei großen Katalogen zu lange dauert.
#!/usr/bin/env bash
# Vor der Suite: definierten Datenbank-Snapshot laden
mysql -u magento -p"$DB_PASSWORD" magento < fixtures/snapshot-baseline.sql
# Pro Test: Transaktion oeffnen, Savepoint setzen
mysql -u magento -p"$DB_PASSWORD" magento -e "START TRANSACTION; SAVEPOINT test_start;"
# Nach dem Test: gezielt zum Savepoint zuruecksetzen statt kompletter Reset
mysql -u magento -p"$DB_PASSWORD" magento -e "ROLLBACK TO SAVEPOINT test_start;"
# Alternative: einzelne, waehrend des Tests angelegte Entitaeten gezielt loeschen
bin/magento customer:delete --email="e2e-*@mironsoft.test"
4. Reihenfolgenunabhängige Tests schreiben
Ein sicheres Zeichen für Reihenfolgenabhängigkeit: Ein Test schlägt fehl, sobald er per --shard oder in zufälliger Reihenfolge ausgeführt wird, obwohl er in der ursprünglichen Datei-Reihenfolge immer bestand. Playwright und Cypress erlauben beide, Tests bewusst in zufälliger Reihenfolge laufen zu lassen, was solche versteckten Abhängigkeiten zuverlässig aufdeckt, lange bevor sie in der Produktions-CI zum Problem werden.
Die Regel ist einfach zu formulieren, aber leicht zu verletzen: Kein Test darf sich auf Seiteneffekte eines anderen Tests verlassen, weder auf angelegte Daten noch auf einen bestimmten Anwendungszustand. Praktisch bedeutet das, jeden Test mit einem eigenen beforeEach-Setup auszustatten, das alle benötigten Voraussetzungen selbst herstellt, statt sich auf die zufällige Reihenfolge der Testdatei zu verlassen. Testsuiten, die regelmäßig randomisiert ausgeführt werden, zwingen das Team dazu, Isolation von Anfang an einzubauen, statt sie nachträglich zu reparieren.
5. Parallel-sichere Testdaten: eindeutige Werte pro Worker
Parallel ausgeführte Test-Worker teilen sich oft dieselbe Backend-Instanz, weshalb feste Testdaten wie test@example.com oder die SKU TEST-001 garantiert zu Kollisionen führen, sobald zwei Worker gleichzeitig denselben Kunden oder dasselbe Produkt anlegen. Das äußert sich meist als sporadischer 409-Conflict-Fehler oder als falscher Datensatz, der von einem anderen Worker überschrieben wurde, und ist damit ein klassischer Flaky-Test-Kandidat.
Die Lösung ist eine Datenfabrik, die pro Testlauf eindeutige Werte generiert, typischerweise aus einer Kombination von Worker-Index, Zeitstempel und Zufallskomponente. Playwright stellt testInfo.workerIndex direkt zur Verfügung, Cypress bietet über Cypress.env() einen ähnlichen Mechanismus mit einer Parallel-ID aus dem CI-Runner. Entscheidend ist, diese eindeutigen Werte konsequent für alle testkritischen Felder zu nutzen: E-Mail-Adressen, SKUs, Kundennummern und Bestellreferenzen, damit zwei parallele Worker niemals denselben Datensatz beanspruchen.
// Playwright: eindeutige Testdaten pro Worker generieren
import { test as base } from '@playwright/test';
type TestData = { email: string; sku: string; customerNo: string };
export const test = base.extend<{ testData: TestData }>({
testData: async ({}, use, testInfo) => {
const uniqueSuffix = `${testInfo.workerIndex}-${Date.now()}`;
const data: TestData = {
email: `e2e-${uniqueSuffix}@mironsoft.test`,
sku: `TEST-SKU-${uniqueSuffix}`,
customerNo: `CUST-${uniqueSuffix}`,
};
await use(data);
},
});
test('legt Kunde mit eindeutiger E-Mail an', async ({ page, testData }) => {
await page.goto('/customer/account/create');
await page.fill('#email', testData.email);
// ... restlicher Testablauf
});
6. Cypress- und Playwright-spezifische Isolationsmechanismen
Playwright isoliert standardmäßig über den BrowserContext, eine leichtgewichtige, komplett getrennte Browser-Sitzung mit eigenem Storage, eigenen Cookies und eigenem Cache, die pro Test oder pro Testdatei neu erzeugt wird. Das ist der entscheidende strukturelle Unterschied zu älteren Cypress-Versionen, die standardmäßig einen einzigen Browser-Tab über die gesamte Spec-Datei hinweg wiederverwendeten.
Cypress hat mit cy.session() seit Version 12 ein vergleichbares Konzept nachgerüstet: Ein Login-Vorgang wird einmal ausgeführt und der resultierende Zustand, Cookies, localStorage und sessionStorage, wird über einen Cache-Schlüssel zwischengespeichert und bei Bedarf validiert, statt bei jedem Test erneut über die UI einzuloggen. Ohne explizites cy.session() oder gezieltes cy.clearCookies() in beforeEach teilen sich Cypress-Tests innerhalb derselben Spec-Datei denselben Browser-Zustand, was Isolation faktisch aushebelt und genau die Symptome aus Abschnitt eins erzeugt.
// Cypress: cy.session fuer schnellen, isolierten Login-Zustand
Cypress.Commands.add('loginAsCustomer', (email, password) => {
cy.session(
[email, password],
() => {
cy.visit('/customer/account/login');
cy.get('#email').type(email);
cy.get('#pass').type(password);
cy.get('#send2').click();
cy.url().should('include', '/customer/account');
},
{
// Vor jeder Wiederverwendung pruefen, ob die Session noch gueltig ist
validate: () => {
cy.getCookie('PHPSESSID').should('exist');
},
cacheAcrossSpecs: false,
}
);
});
it('zeigt den Kundenbereich nach Login', () => {
cy.loginAsCustomer('e2e-worker0@mironsoft.test', 'Test1234!');
cy.visit('/customer/account');
cy.contains('Mein Konto').should('be.visible');
});
7. Teardown- und Cleanup-Strategien für Testdaten
Cleanup nach dem Test ist ebenso wichtig wie Setup davor, wird aber häufiger vernachlässigt, weil ein fehlender Teardown-Schritt den aktuellen Testlauf selten sofort sichtbar bricht, sondern erst spätere Läufe verunreinigt. Ein robustes Muster ist, jeden über die API angelegten Datensatz mit einer eindeutigen ID zu erfassen und in einem afterEach- oder afterAll-Hook gezielt wieder zu löschen, statt sich auf einen globalen Datenbank-Reset zu verlassen, der bei großen Testsuiten Minuten kostet.
Playwright-Fixtures mit automatischem Teardown über die use()-Funktion und Cypress-Tasks, die über cy.task() direkt mit dem Backend sprechen, sind beide geeignete Orte für Cleanup-Logik, weil sie unabhängig vom Testergebnis ausgeführt werden, auch wenn eine Assertion fehlschlägt. Ergänzend hilft ein nächtlicher CI-Job, der verwaiste Testdaten anhand eines Namens-Präfixes wie e2e- identifiziert und aufräumt, als Sicherheitsnetz für Fälle, in denen ein abgebrochener Testlauf den regulären Teardown übersprungen hat.
# CI-Pipeline: parallele Shards plus Cleanup-Job als Sicherheitsnetz
jobs:
e2e-tests:
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- run: npx playwright test --shard=${{ matrix.shard }}/4
env:
CI: true
nightly-cleanup:
needs: e2e-tests
if: always()
steps:
- name: Verwaiste Testdaten entfernen
run: bin/magento customer:delete --email-prefix="e2e-" --older-than="24h"
8. Flaky Tests diagnostizieren: Shared State als Ursache
Ein Test, der in etwa fünf von hundert Läufen fehlschlägt, ohne dass sich Testcode oder Anwendung geändert haben, ist der zuverlässigste Indikator für Shared State. Der erste Diagnoseschritt ist, den verdächtigen Test isoliert zehnmal hintereinander laufen zu lassen und danach denselben Test zehnmal parallel mit anderen Tests, die dieselbe Ressource wie Kundendaten oder Produktkatalog anfassen, um zu prüfen, ob die Fehlerrate mit der Nebenläufigkeit steigt.
Playwright-Traces und Cypress-Videos aus fehlgeschlagenen CI-Läufen zeigen oft den entscheidenden Hinweis: einen unerwarteten Zustand beim Start des Tests, etwa einen bereits eingeloggten Nutzer oder einen Warenkorb, der nicht leer sein sollte. Ein Logging-Statement, das zu Beginn jedes Tests den aktuellen Zustand von Cookies, localStorage und relevanten Datenbank-Zeilen protokolliert, macht Shared-State-Lecks innerhalb weniger CI-Läufe sichtbar, statt tagelang stichprobenartig zu debuggen.
9. Isolationsmuster im Vergleich: schlecht vs. gut
Die folgende Übersicht stellt verbreitete Anti-Patterns bei der Testisolation den empfohlenen Alternativen gegenüber und zeigt, warum scheinbar bequeme Abkürzungen langfristig zu instabilen Suiten führen.
| Bereich | Schlechtes Muster | Risiko | Empfohlener Ansatz |
|---|---|---|---|
| Browser-Storage | Storage nicht zwischen Tests geleert | Falscher Warenkorb-/Login-Zustand | Neuer BrowserContext / clearCookies() |
| Testdaten | Feste E-Mail/SKU für alle Tests | Kollisionen bei paralleler Ausführung | Eindeutige Daten pro Worker generieren |
| Datenbank | Kompletter Reset nach jedem Test | Lange Laufzeiten, teure CI | Transaktions-Rollback pro Test |
| Reihenfolge | Test B verlässt sich auf Daten von Test A | Bricht bei Sharding/Randomisierung | Jeder Test stellt eigene Voraussetzungen her |
| Login-Zustand | UI-Login in jedem Test wiederholt | Langsame, redundante Tests | cy.session() / gespeicherter Storage-State |
In der Praxis verstärken sich diese Muster gegenseitig: Wer Testdaten nicht eindeutig macht, kann Tests nicht gefahrlos parallelisieren, und wer nicht parallelisiert, verliert den Geschwindigkeitsvorteil, der Cypress und Playwright in CI-Pipelines überhaupt erst attraktiv macht. Konsequente Isolation auf allen vier Ebenen, Storage, Datenbank, Testdaten und Reihenfolge, ist damit keine Kür, sondern Voraussetzung für schnelle, verlässliche E2E-Suiten.
Mironsoft
E2E-Testautomatisierung, Cypress und Playwright für Magento- und Hyvä-Shops
Stabile E2E-Suiten ohne Flaky Tests?
Wir bauen und härten eure Cypress- und Playwright-Suiten für Magento- und Hyvä-Shops, mit sauberer Test-Isolation, parallel-sicheren Testdaten und stabilen CI-Pipelines statt willkürlichen Retries.
Flaky-Test-Audit
Ursachenanalyse für instabile Tests und Shared-State-Lecks
Isolations-Setup
Browser-Kontexte, Datenfabriken und Datenbank-Rollback pro Test
CI/CD-Integration
Parallele Shards, Cleanup-Jobs und stabile Pipelines
10. Zusammenfassung
Test-Isolation zwischen Testläufen löst ein Kernproblem: Instabile Suiten kosten Vertrauen ins Testsystem und CI-Zeit durch unnötige Retries. Wer Browser-Storage über eigene Contexts oder saubere cy.session()-Nutzung isoliert, verhindert, dass Login- oder Warenkorb-Zustand zwischen Tests durchsickert. Wer Datenbank-Zustand über Transaktions-Rollback statt kompletter Resets isoliert, hält Testläufe schnell und trotzdem sauber.
Parallel-sichere Testdaten mit eindeutigen E-Mails, SKUs und Kundennummern pro Worker sind die Voraussetzung dafür, dass Parallelisierung überhaupt einen Geschwindigkeitsvorteil bringt, statt neue Kollisionen zu erzeugen. Konsequentes Cleanup nach jedem Test und ein nächtlicher Sicherheitsnetz-Job verhindern, dass sich verwaiste Testdaten über Wochen ansammeln und spätere Läufe unbemerkt verfälschen.
Test-Isolation zwischen Testläufen - Das Wichtigste auf einen Blick
Browser-Storage isolieren
Eigener BrowserContext pro Test in Playwright, clearCookies()/clearLocalStorage() in Cypress.
Backend-Zustand isolieren
Transaktions-Rollback pro Test statt kompletter Datenbank-Reset, gezieltes Cleanup einzelner Entitäten.
Parallel-sichere Testdaten
Eindeutige E-Mails, SKUs und Kundennummern pro Worker-Index, keine festen Testwerte.
Cleanup & Diagnose
Gezielter Teardown nach jedem Test, nächtlicher Cleanup-Job als Sicherheitsnetz gegen Shared State.