Was End-to-End-Testing eigentlich bedeutet
End-to-End-Testing steuert das echte System aus der Sicht eines Nutzers. Anstatt eine Funktion oder eine Route isoliert aufzurufen, öffnet ein e2e-Test die Anwendung, führt dieselben Schritte aus, die auch ein Mensch tun würde, und prüft, ob das Ergebnis, das ein Nutzer sehen würde, tatsächlich eingetreten ist.
Diese Definition hat zwei Konsequenzen. Erstens überschreitet der Test jede Grenze im System: Browser, Frontend-Code, HTTP, API, Datenbank und zurück. Zweitens beziehen sich die Assertions auf das Verhalten, nicht auf die Implementierung. Man prüft, ob eine Bestätigung sichtbar ist, und nicht, ob eine bestimmte Funktion aufgerufen wurde.
Da der gesamte Stack involviert ist, kommen e2e-Tests einer Garantie am nächsten, dass ein Nutzer eine Aufgabe erfolgreich abschließen kann. Sie sind jedoch auch die langsamsten und teuersten Tests, die man schreibt – und genau deshalb schreibt man nur wenige davon und wählt diese sorgfältig aus.
Wo e2e in die Testpyramide passt
Die Testpyramide ist eine Faustregel dafür, wie viele Tests welcher Art geschrieben werden sollten. Die Basis ist breit: viele schnelle Unit-Tests. Die Mitte ist schmaler: Integrationstests über einige wenige Komponenten hinweg. Die Spitze ist klein: eine Handvoll End-to-End-User-Journeys.
- Unit-Tests dauern Millisekunden, laufen ohne Infrastruktur und fixieren reine Logik sowie Edge-Cases.
- Integrationstests dauern Sekunden, prüfen eine Route oder ein Modul mit echten Collaborators und finden Fehler in der Verdrahtung.
- End-to-End-Tests dauern mehrere zehn Sekunden, laufen gegen einen deployten Stack und beweisen, dass ein Nutzer eine kritische Aufgabe abschließen kann.
Die Form ist entscheidend, da die Kosten steigen und das Feedback langsamer wird, je weiter man nach oben geht. Ein Bug, den ein Unit-Test finden kann, sollte auch durch einen Unit-Test gefunden werden. Reservieren Sie e2e für die Journeys, bei denen ein defekter Pfad ein defektes Produkt bedeutet: Registrierung, Login, Checkout, Veröffentlichen, Einladen. Wenn ein Test keinen Browser benötigt, um sinnvoll zu sein, sollte er wahrscheinlich auch keinen verwenden.
Ein nützlicher Sanity-Check: Fragen Sie bei jedem e2e-Test, welcher Unit- oder Integrationstest denselben Bug hätte finden können. Wenn die Antwort lautet: „einer, der einfach zu schreiben ist“, dann befindet sich der e2e-Test in der falschen Schicht.
Die richtigen User Journeys für die Automatisierung auswählen
Man kann nicht jeden einzelnen Pfad automatisieren. Konzentrieren Sie sich daher auf die Bereiche, in denen Ausfälle die höchsten Kosten verursachen und in denen Fehler am wahrscheinlichsten durch die darunterliegenden Testebenen schlüpfen.
Bewerten Sie eine potenzielle Journey anhand von zwei Achsen: Business Impact und Integrationsrisiko. Dort, wo sowohl der Impact als auch das Risiko hoch sind, spielt e2e seine volle Stärke aus.
- Hoher Impact, hohes Risiko — Registrierung, Login, Checkout, Passwort-Reset. Automatisieren Sie diese zuerst.
- Hoher Impact, geringes Risiko — die Marketing-Homepage. Decken Sie diese mit einem Smoke Test ab, nicht mit einer vollständigen Journey.
- Geringer Impact, hohes Risiko — ein Admin-Export, der monatlich genutzt wird. Ein einzelner Test oder ein geskripteter Check ist hier ausreichend.
- Geringer Impact, geringes Risiko — Einstellungen-Toggles und kosmetische Zustände. Überlassen Sie diese den Unit- und Component-Tests.
Die Journeys, die die meisten Systemgrenzen überschreiten, sind am wertvollsten, da sie genau die Bereiche abdecken, die ein Unit Test nicht erreichen kann. Ein Checkout berührt den Warenkorb, die Preisberechnung, das Payment, den Order-Service und E-Mails — genau die Verknüpfungen, die stillschweigend kaputtgehen, wenn ein Team ein Feld umbenennt.
Schreiben Sie die Liste auf und überarbeiten Sie diese jedes Quartal. Die Menge der kritischen Journeys ändert sich mit dem Wachstum des Produkts, und der wichtige Flow von gestern kann heute bereits Dead Code sein.
Die Wahl des Tools: Playwright oder Cypress
Zwei Tools dominieren das moderne Browser-Testing, und beide sind hervorragend.
Playwright steuert Chromium, Firefox und WebKit über eine einzige API an. Es führt Tests standardmäßig in parallelen Worker-Prozessen aus, unterstützt mehrere Browser-Projekte, verfügt über Locators mit Auto-Waiting und liefert einen Trace Viewer mit, der einen Durchlauf Frame für Frame aufzeichnet. Sein storageState macht die Wiederverwendung der Authentifizierung einfach, und sein request Fixture stellt Ihnen einen API-Client innerhalb desselben Tests zur Verfügung. Es ist derzeit der gängige Standard für neue Test-Suites.
Cypress läuft direkt im Event Loop des Browsers, wodurch sich das Debugging unmittelbar anfühlt: Sie können den DOM zu jedem beliebigen Zeitpunkt untersuchen und im Runner durch die Befehle „zeitreisen“. Die API ist einsteigerfreundlich und die Fehlermeldungen sind exzellent. Der Kompromiss besteht darin, dass die Cross-Browser-Unterstützung und die Parallelisierung historisch gesehen einen höheren Setup-Aufwand erforderten.
Die Wahl des Tools ist selten so entscheidend wie die Disziplin bei der Umsetzung. Sowohl eine gut geschriebene Playwright-Suite als auch eine gut geschriebene Cypress-Suite funktionieren einwandfrei. Entscheiden Sie sich für eines, lernen Sie das jeweilige Waiting-Modell und investieren Sie Ihre Energie in die Isolation der Tests und die Wahl der richtigen Locators.
Die Anatomie eines Tests
Fast jeder e2e Test besteht aus denselben drei Teilen, die üblicherweise als Arrange, Act und Assert bezeichnet werden.
test("a signed-in user can upgrade to Pro", async ({ page }) => {
// arrange: the account and plan already exist
// act: perform the journey
await page.goto("/pricing");
await page.getByRole("button", { name: "Choose Pro" }).click();
// assert: the user sees the result
await expect(
page.getByRole("heading", { name: "You're on Pro" }),
).toBeVisible();
});
Halten Sie den Arrange-Schritt nach Möglichkeit außerhalb der UI. Einen Benutzer zu erstellen, indem man sich durch ein Registrierungsformular klickt, verlängert die Laufzeit einer Test-Suite um Minuten und koppelt jeden einzelnen Test an den Registrierungsflow. Erstellen Sie den Benutzer stattdessen über die API und starten Sie den Test bereits im eingeloggten Zustand.
Eine weitere Gewohnheit, die sich lohnt: Pro Test nur eine User Journey. Wenn ein Test fünf verschiedene Dinge abdeckt und fehlschlägt, wissen Sie nur, dass eines dieser fünf Dinge kaputt ist. Wenn er nur eines abdeckt, ist der Name des fehlgeschlagenen Tests bereits die Diagnose.
Eine Journey schreiben, die gut lesbar ist
Ein e2e-Test wird weitaus häufiger gelesen als geschrieben. Ein Leser sollte erkennen können, was genau geschützt wird, ohne die App öffnen zu müssen.
Benennen Sie den Test nach dem Ergebnis für den Nutzer, nicht nach der Implementierung. „ein angemeldeter Nutzer kann auf Pro upgraden“ ist besser als „test checkout button click“. Verwenden Sie die Gegenwart und das Vokabular des Nutzers.
Strukturieren Sie den Test als eine Sequenz von Nutzeraktionen, eine pro Zeile, mit Leerzeilen zwischen den einzelnen Phasen.
test("an admin can invite a teammate", async ({ page }) => {
await page.goto("/team");
await page.getByRole("button", { name: "Invite" }).click();
await page.getByLabel("Email").fill("[email protected]");
await page.getByRole("button", { name: "Send invite" }).click();
await expect(page.getByText("Invitation sent")).toBeVisible();
});
Lagern Sie wiederkehrende Journeys in Helper aus, aber halten Sie den Test selbst lesbar. Ein Helper namens upgradeToPro(page) ist gut; ein Helper, der den gesamten Test versteckt, hingegen nicht. Der Test sollte die einzelnen Schritte weiterhin aufzeigen.
Kommentieren Sie abschließend das „Warum“, nicht das „Was“. Der Code sagt bereits aus, welcher Button geklickt wurde; ein Kommentar ist nur dann nützlich, wenn er erklärt, warum der Test existiert oder warum ein Schritt ungewöhnlich aussieht.
Locators: Sprechen Sie mit dem Nutzer, nicht mit dem DOM
Ein Locator ist die Art und Weise, wie der Test ein Element findet. Die größte Verbesserung, die Sie an einer e2e-Suite vornehmen können, besteht darin, Locators so zu wählen, wie es ein Nutzer tun würde.
Playwright ordnet diese von der bevorzugten bis zur am wenigsten bevorzugten Methode:
getByRole— Buttons, Links, Überschriften und Inputs über die accessible role und den Namen.getByLabel— Formularsteuerelemente über ihr Label.getByText— Sichtbarer Text.getByPlaceholder,getByAltText,getByTitle— Andere sichtbare Attribute.getByTestId— Eine stabile Test-ID, wenn nichts Nutzerorientiertes passt.
await page.getByRole("button", { name: "Add to cart" }).click();
await page.getByLabel("Email").fill("[email protected]");
await expect(page.getByRole("heading", { name: "Your cart" })).toBeVisible();
Diese Reihenfolge ist nicht ästhetisch bedingt. Role- und Label-Locators schlagen fehl, wenn ein Element nicht barrierefrei ist, sodass der Test gleichzeitig als Accessibility-Check dient. Zudem überstehen sie Refactorings: Das Umbenennen einer CSS-Klasse bringt sie nicht zum Absturz, bricht hingegen .btn.btn--primary > span.
Nutzen Sie getByTestId bewusst und nicht als Standard. Eine Test-ID ist ein Vertrag, den Sie pflegen müssen, und eine Suite voller solcher IDs sagt nichts darüber aus, ob das Interface sinnvoll gestaltet ist.
Authentifizierung handhaben
Sich vor jedem Test über die UI anzumelden, ist die häufigste Ursache für langsame und instabile e2e-Suites. Ein Login besteht aus einem Formular, einem Netzwerk-Roundtrip und einem Redirect – und das hunderte Male wiederholt.
Die Lösung besteht darin, sich einmal zu authentifizieren und die Session wiederzuverwenden. Playwright nennt die gespeicherte Session storageState — Cookies und Local Storage, die in eine Datei serialisiert werden.
// tests/auth.setup.ts
import { test as setup, expect } from "@playwright/test";
setup("authenticate", async ({ page }) => {
await page.goto("/login");
await page.getByLabel("Email").fill(process.env.E2E_EMAIL!);
await page.getByLabel("Password").fill(process.env.E2E_PASSWORD!);
await page.getByRole("button", { name: "Sign in" }).click();
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
await page.context().storageState({ path: "playwright/.auth/user.json" });
});
Ein Setup-Projekt schreibt die Datei, und die Browser-Projekte hängen von dieser ab und laden sie über use.storageState. Tests, die einen anderen Benutzer benötigen, erhalten eine andere State-Datei.
// playwright.config.ts
export default defineConfig({
projects: [
{ name: "setup", testMatch: /auth\.setup\.ts/ },
{
name: "chromium",
use: { storageState: "playwright/.auth/user.json" },
dependencies: ["setup"],
},
],
});
Du solltest dennoch einen Test behalten, der die Anmeldung über die Benutzeroberfläche durchführt. Das Login-Formular ist ein eigenständiger User Journey, und der Shortcut sollte niemals die einzige Testabdeckung dafür sein.
Tests deterministisch gestalten
Determinismus ist das A und O. Ein Test, der nur manchmal besteht, ist schlimmer als einer, der immer fehlschlägt, da er das Team dazu gewöhnt, rote Tests zu ignorieren.
Drei Regeln decken den Großteil ab.
Niemals sleep verwenden. waitForTimeout(3000) ist entweder zu kurz und führt zu Flakiness oder zu lang und macht die Tests langsam. Warte stattdessen auf die Bedingung: await expect(locator).toBeVisible(). Web-first Assertions versuchen es so lange wiederholt, bis sie erfolgreich sind oder ein Timeout eintritt – genau das, was man möchte.
Das Netzwerk kontrollieren. Mocke Drittanbieter und instabile Abhängigkeiten mit page.route, damit eine langsame Payment-Sandbox nicht deine gesamte Test-Suite zum Scheitern bringt. Prüfe die Anfragen, die deine App stellt, wenn dies das zu testende Verhalten ist.
await page.route("**/api/recommendations", (route) =>
route.fulfill({ json: { items: [] } }),
);
Zeit und Zufall einfrieren. Ein Test, der von „heute“ oder einer zufälligen ID abhängt, wird an bestimmten Grenzwerten fehlschlagen. Injiziere eine Clock oder setze feste Seed-Werte für die Daten, die der Test liest.
Dann gibt es noch den Browser selbst: Animationen, Autofocus und Transitions erzeugen Race Conditions, die wie Anwendungsfehler aussehen. Deaktiviere Animationen in der Testumgebung, wann immer möglich, und bevorzuge Assertions auf den finalen Zustand.
Cross-browser- und Responsive-Abdeckung
Ein User Journey, der in Chromium funktioniert, kann in WebKit fehlschlagen, und ein Layout, das auf einem Laptop gut aussieht, kann auf einem Smartphone unbrauchbar sein. Die Abdeckung verschiedener Browser und Viewports ist einer der wenigen Bereiche, in denen e2e Dinge leistet, die kein anderes Tool kann.
Playwright macht dies zu einer reinen Konfigurationsaufgabe. Definieren Sie pro Browser ein Projekt und verwenden Sie dieselben Tests mehrfach.
// playwright.config.ts
export default defineConfig({
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
{ name: "mobile", use: { ...devices["iPhone 13"] } },
],
});
Nicht jeder Test muss in jedem Browser laufen. Führen Sie die gesamte Suite in Chromium aus, um schnelles Feedback zu erhalten, und lassen Sie die kritischen Journeys über alle Projekte hinweg nach einem Zeitplan oder vor dem Release laufen. So bleiben Pull Requests schnell, ohne dass die Abdeckung darunter leidet.
Wenn ein Bug browserspezifisch ist, fügen Sie einen Regressionstest im betroffenen Projekt hinzu. Ein Kommentar, der auf das entsprechende Issue verlinkt, ist wertvoller als eine vage Notiz wie „Safari ist eigenartig“.
Testdaten verwalten
Daten sind der Punkt, an dem e2e-Suites schleichend unzuverlässig werden. Zwei Tests, die beide einen Benutzer namens [email protected] erstellen, werden einzeln bestehen, aber gemeinsam fehlschlagen.
Nutzen Sie die API für das Seeding. Das ist schneller als über die UI, führt zu deutlichen Fehlermeldungen, wenn das Seeding fehlschlägt, und hält den Test auf den eigentlichen User-Journey fokussiert statt auf das Setup.
const res = await request.post("/api/test/users", {
data: { email: `ada+${Date.now()}@example.com` },
});
expect(res.ok()).toBeTruthy();
const user = await res.json();
Isolieren Sie Daten pro Test oder pro Worker. Einzigartige E-Mails, Tenant-gebundene Datensätze oder ein frischer Namespace pro Worker funktionieren alle. Entscheidend ist, dass keine zwei Tests von derselben Zeile abhängen.
Bereinigen Sie alles, was Sie erstellen, oder machen Sie ein Cleanup überflüssig, indem Sie Daten an einen Testlauf binden und den gesamten Scope am Ende löschen. Verwaiste Datensätze sammeln sich an, verlangsamen die Datenbank und führen letztlich zu Fehlern, die nichts mit der aktuellen Änderung zu tun haben.
Ausführung gegen einen echten Stack
Ein e2e-Test benötigt eine Umgebung zum Ausführen: ein Frontend, eine API und eine Datenbank, alle in derselben Code-Version. Die reproduzierbarste Option ist ein docker compose Stack, der das gesamte Setup mit einem einzigen Befehl startet.
docker compose -f docker-compose.e2e.yml up -d --wait
pnpm exec playwright test
docker compose -f docker-compose.e2e.yml down -v
Das --wait Flag ist hier entscheidend: Es sorgt dafür, dass Compose erst dann zurückkehrt, wenn die Health-Checks erfolgreich waren, wodurch Race-Conditions wie „Connection refused beim ersten Test“ vermieden werden. Das -v beim Beenden löscht die Volumes, sodass der nächste Durchlauf sauber startet.
Preview-Umgebungen gehen noch einen Schritt weiter. Eine Plattform baut den Branch, deployt ihn unter einer temporären URL und stellt ihn dem Test-Job zur Verfügung. Das kommt einer Produktionsumgebung am nächsten und ermöglicht es dem Produktmanagement und der QA, genau denselben Build durchzuklicken, den auch die Tests verwenden.
Richten Sie die Test-Suite über eine Umgebungsvariable — BASE_URL — auf die entsprechende Umgebung aus und hardcoden Sie niemals einen Host. Dieselbe Suite sollte lokal, in der CI und gegen eine Preview-Umgebung laufen.
Wenn der User-Flow eine API und kein Browser ist
Nicht jeder End-to-End-Flow benötigt einen Browser. Ein Webhook, ein Background-Job, eine CLI oder ein Service-to-Service-Flow sind alle im entscheidenden Sinne „End-to-End“: Sie testen das reale System.
Die request fixture von Playwright ist ein vollständiger API-Client, sodass ein API-Flow fast identisch mit einem Browser-Flow aussieht.
test("a paid order is fulfilled end to end", async ({ request }) => {
const order = await request.post("/api/orders", {
data: { sku: "pro-plan", quantity: 1 },
});
expect(order.status()).toBe(201);
await request.post("/api/payments/webhook", {
data: { orderId: (await order.json()).id, status: "paid" },
});
const res = await request.get(`/api/orders/${(await order.json()).id}`);
expect(await res.json()).toMatchObject({ status: "fulfilled" });
});
Diese Tests sind schneller und weitaus weniger fehleranfällig (flaky) als Browser-Tests. Nutzen Sie sie daher für Flows, die tatsächlich keine UI benötigen. Für eine reine API-Abdeckung auf einer niedrigeren Ebene ist Supertest das schlankere Tool.
E2E in der CI ausführen
E2E-Tests gehören in Pull Requests und vor einem Release, nicht bei jedem Speichern. In der CI machen ein paar Flags den Unterschied zwischen einem nützlichen Gate und einem, das ignoriert wird.
- Headless ausführen. Es gibt keinen Bildschirm; Playwright und Cypress nutzen in der CI standardmäßig den headless-Modus.
- Die Suite sharden. Verteilen Sie Dateien mit
--shard=1/3auf mehrere Maschinen, damit die Gesamtlaufzeit konstant bleibt, während die Suite wächst. - Einmalig wiederholen. Ein einzelner Retry unterscheidet einen echten Fehler von einem Infrastruktur-Hickup – vorausgesetzt, Sie tracken die Retry-Rate, anstatt Flakiness dadurch zu verschleiern.
- Artefakte bei Fehlern hochladen. Traces, Screenshots und Videos sorgen dafür, dass ein roter Build in der CI diagnostizierbar wird, ohne dass eine lokale Reproduktion nötig ist.
- run: pnpm exec playwright test --shard=${{ matrix.shard }}/3 --retries=1
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: report-${{ matrix.shard }}
path: playwright-report/
Retries sind ein Sicherheitsnetz, keine Lösung. Wenn ein Test einen Retry benötigt, um konsistent zu bestehen, liegt ein echtes Race-Condition- oder Isolationsproblem vor; der Retry verschafft Ihnen lediglich Zeit, den Fehler zu finden.
Einen fehlgeschlagenen Run debuggen
Die erste Frage nach einem Fehler ist immer dieselbe: Was hat der Browser tatsächlich gesehen? Ein Tool, das diese Frage beantworten kann, ohne dass man den Fehler lokal reproduzieren muss, verwandelt eine zweistündige Untersuchung in eine zweiminütige.
Playwright zeichnet einen Trace auf – Screenshots, DOM-Snapshots, Netzwerk und Konsole – und der Trace Viewer spielt den Run Schritt für Schritt ab. Aktivieren Sie Traces bei Fehlern, öffnen Sie dann den Report und spulen Sie zu dem Moment, in dem die Assertion fehlgeschlagen ist.
pnpm exec playwright test --trace on-first-retry
pnpm exec playwright show-trace test-results/checkout/trace.zip
Lassen Sie die Suite lokal im Headed Mode oder im UI Mode laufen, um den Test zu beobachten und zu pausieren. Cypress bietet dasselbe Konzept über seinen interaktiven Runner an, der ein Command Log führt, durch das man mittels Time-Travel navigieren kann.
pnpm exec playwright test --ui
pnpm exec playwright test --headed --debug
Die Gewohnheit, die das Debugging beschleunigt, besteht darin, genau das zu prüfen (assert), was tatsächlich falsch ist. Ein Fehler bei expect(page).toHaveURL(/\/checkout/) sagt Ihnen, dass die Navigation nie stattgefunden hat – das ist eine völlig andere Untersuchung als ein Fehler bei der Bestätigungsüberschrift.
Flakiness bekämpfen
Flakiness ist die „Steuer“, die man für e2e-Tests zahlt, und sie hat fast immer eine von wenigen Ursachen.
- Ein fehlendes await oder eine Race Condition. Der Test hat agiert, bevor die App stabil war. Beheben Sie dies mit einer web-first assertion, nicht mit einem sleep.
- Gemeinsam genutzte Daten. Zwei Tests haben auf dieselbe Zeile zugegriffen. Isolieren Sie die Daten pro Test oder pro Worker.
- Eine externe Abhängigkeit. Ein Drittanbieter-Skript oder eine API war langsam. Nutzen Sie Mocks oder nehmen Sie die Abhängigkeit aus dem kritischen Pfad heraus.
- Ein fragiler Locator. Der Test war an ein Markup gebunden, das sich geändert hat. Wechseln Sie zu einem Role- oder Label-Locator.
- Eine Animation. Das Element war zwar vorhanden, aber noch nicht stabil. Deaktivieren Sie Animationen in der Testumgebung.
Der Weg zur Behebung von Flakiness besteht darin, jeden „Flake“ als Bug mit einer konkreten Ursache zu behandeln, ihn zu isolieren und die Ursache zu beheben. Eine Testsuite, die sich durch Retries ins Grüne rettet, ist eine Suite, der niemand vertraut – und eine Suite, der niemand vertraut, ist schlimmer als gar keine Suite.
Die Test-Suite langfristig gesund halten
Eine e2e-Suite neigt natürlicherweise dazu, so lange zu wachsen, bis sie langsam wird und ständig Fehler anzeigt. Sie gesund zu halten, ist ein kontinuierlicher Prozess und kein einmaliges Setup.
Überprüfen Sie die Suite genauso wie Ihr Produkt. Wenn ein Feature entfernt wird, löschen Sie den dazugehörigen Test im selben Pull Request. Ein Test, der nichts mehr absichert, verursacht nur unnötige Kosten.
Behalten Sie die relevanten Kennzahlen im Auge: die Gesamtlaufzeit, die Erfolgsquote und die Retry-Rate. Eine steigende Retry-Rate ist das früheste Signal dafür, dass Flakiness einzieht – lange bevor die Suite sichtbar unzuverlässig wird.
Übernehmen Sie die Verantwortung für die Suite. Eine gemeinsame e2e-Suite ohne klaren Owner driftet ab: Fehler werden einfach neu gestartet, Tests werden übersprungen und innerhalb eines Quartals vertraut niemand mehr einem roten Run. Legen Sie eine Rotation fest oder weisen Sie eine kleine Gruppe zu, um die Suite grün zu halten. Machen Sie das Beheben eines flaky Tests zu einer prioritären Aufgabe und nicht zu einer bloßen Unterbrechung.
Halten Sie die Suite schnell genug, um sie bei jedem Pull Request auszuführen. Wenn sie dafür zu groß wird, nutzen Sie Sharding, parallelisieren Sie die Ausführung oder verschieben Sie die weniger kritischen User Journeys in einen nächtlichen Run. Ein Quality Gate, das eine Stunde dauert, ist ein Quality Gate, das die Leute umgehen.
Was nicht mit e2e abgedeckt werden sollte
Es ist verlockend, alles über den Browser zu testen, weil es sich wie die reale Anwendung anfühlt. Widerstehen Sie diesem Impuls. E2E ist die teuerste Testschicht; setzen Sie sie daher nur dort ein, wo sie wirklich einen Mehrwert bietet.
Decken Sie Folgendes nicht mit e2e ab:
- Reine Logik und Edge Cases. Datumsformatierung, Preisberechnungen, Validierungsregeln – all das gehört in Unit-Tests, die in Millisekunden ausgeführt werden.
- Jeden einzelnen Fehlerpfad. Eine 500er-Seite rechtfertigt einen Testlauf; die zwanzig verschiedenen Arten, wie die API fehlschlagen kann, sind Gegenstand von Integration-Tests.
- Umfassende Input-Kombinationen. E2E deckt den repräsentativen Pfad ab, nicht die gesamte Matrix.
- Komponenten-Verhalten. Dass sich ein Dropdown öffnet, gehört in einen Komponenten-Test, da dieser schneller und präziser ist.
- Alles, was bereits auf einer niedrigeren Ebene abgedeckt ist. Die Duplizierung eines Unit-Tests auf der e2e-Ebene erhöht die Kosten und führt neue Fehlerquellen ein, ohne die Sicherheit zu erhöhen.
Die Faustregel lautet: Wenn ein Bug ohne Browser gefunden werden kann, finden Sie ihn ohne Browser.
Best Practices
- Schreiben Sie nur wenige e2e Tests und machen Sie jeden einzelnen zu einem kritischen User Journey.
- Seed-Daten über die API einspielen und Tests bereits authentifiziert starten.
- Nutzen Sie Role-, Label- und Text-Locators; verwenden Sie Test-IDs nur als letzte Instanz.
- Warten Sie mit Web-First Assertions auf Bedingungen, niemals mit
waitForTimeout. - Isolieren Sie Daten pro Test oder pro Worker, damit die Reihenfolge keine Rolle spielt.
- Behalten Sie einen Journey pro Test bei, damit ein Fehler direkt die Ursache benennt.
- Richten Sie die Suite auf
BASE_URLaus; führen Sie sie lokal, in der CI und in Previews aus. - Erfassen Sie Traces und Videos bei Fehlern und laden Sie diese als CI-Artefakte hoch.
- Quarantänisieren und beheben Sie Flakes; akzeptieren Sie Retries niemals als Lösung.
- Behalten Sie mindestens einen echten Login-Test bei, auch wenn Sie
storageStatewiederverwenden.
Häufige Fehler
- Vor jedem Test der Login über die UI.
- Auswahl von Elementen über CSS-Klassen oder die DOM-Position.
- Hinzufügen von
waitForTimeout, um eine Race Condition zu “beheben”. - Verwendung desselben seeded Users oder Records über mehrere Tests hinweg.
- Tests gegen einen manuell gestarteten lokalen Server statt gegen einen reproduzierbaren Stack.
- Testen von Logik auf Unit-Ebene über den Browser.
- Hard-coding von
localhost:3000, sodass die Suite nur auf einer Maschine läuft. - Keine Artifacts hochladen und dadurch CI-Fehler nicht debuggen können.
- Flaky Tests im Zustand “rot-und-retry” lassen, anstatt sie in Quarantäne zu stellen.
Wie geht es weiter?
E2E-Tests bilden die Spitze der Pyramide und sind am effektivsten, wenn die darunterliegenden Schichten stabil sind. Lesen Sie Supertest für schnelle API-Tests, die den Großteil Ihrer HTTP-Oberfläche abdecken sollten, und Playwright für einen tieferen Einblick in das Tool selbst. Falls Sie Alternativen prüfen möchten: Cypress behandelt den anderen großen Test-Runner, und Docker erklärt, wie Sie den reproduzierbaren Stack aufsetzen, von dem Ihre Test-Suite abhängt.