End-to-End Testing

End-to-End Testing

End-to-End-Tests steuern das reale System so, wie es ein Nutzer tun würde. Sie sind langsam, in geringer Zahl vorhanden und unersetzlich: Sie sind die einzigen Tests, die beweisen können, dass eine Person eine Aufgabe tatsächlich abschließen kann.

intermediate15 min readUpdated 16. Sept. 2026
checkout.spec.ts
ts
// tests/checkout.spec.ts
import { test, expect } from "@playwright/test";

test("a signed-in user can buy a plan", async ({ page }) => {
  await page.goto("/pricing");

  await page.getByRole("button", { name: "Choose Pro" }).click();
  await page.getByLabel("Card number").fill("4242 4242 4242 4242");
  await page.getByRole("button", { name: "Pay" }).click();

  await expect(
    page.getByRole("heading", { name: "You're on Pro" }),
  ).toBeVisible();
});
Scope
Das gesamte System
Geschwindigkeit
Sekunden pro Test
Anzahl
Wenige, aber hoher Wert
Gängige Tools
Playwright, Cypress
Läuft in
Einem echten Browser
Überwacht
Kritische User Journeys

Warum es wichtig ist

Was e2e-Testing dir bringt

Vertrauen in den gesamten Pfad

Ein e2e-Test durchläuft die UI, die API, die Datenbank und zurück. Er ist somit der einzige Test, der beweisen kann, dass ein Nutzer eine Aufgabe tatsächlich abschließen kann.

Ein Sicherheitsnetz gegen Regressionen

Eine Handvoll Journey-Tests fangen Integrationsfehler ab, die Unit-Tests übersehen: ein umbenanntes Feld, ein defekter Redirect, eine fehlende Migration.

Ausführbare Dokumentation

Eine lesbare Spec beschreibt das Produkt in der Sprache des Nutzers und im Gegensatz zu einer Wiki-Seite lässt sie den Build fehlschlagen, wenn sie veraltet ist.

Das Gesamtbild

Die drei Ebenen eines e2e-Tests

Die Welt vorbereiten, den Pfad des Nutzers steuern und prüfen, was eine Person sehen würde.

Der Stack

Arrange

Eine echte Datenbank, API und Frontend, die in einen bekannten Zustand versetzt wurden. Wenn die Umgebung nicht reproduzierbar ist, kann keine Assertion vertraut werden.

Die Journey

Act

Steuere die Oberfläche so, wie es eine Person tun würde – über Rollen, Labels und sichtbaren Text statt über Implementierungsdetails.

Das Ergebnis

Assert

Prüfe, was der Nutzer sieht, und bestätige bei Bedarf den Seiteneffekt über die API oder die Datenbank.

HTML5 auf einen Blick

Das e2e-Toolkit

Navigieren

page.goto lädt eine Route und der Test beginnt bei einer echten URL.

Lokalisieren

getByRole, getByLabel und getByText finden Elemente so, wie ein Nutzer es tun würde.

Agieren

click, fill, type und select steuern die Benutzeroberfläche.

Prüfen

expect(locator).toBeVisible() wiederholt den Versuch, bis das Ergebnis wahr ist.

Authentifizieren

Nutze einen gespeicherten storageState anstatt dich in jedem Test neu einzuloggen.

Beobachten

Warte auf Antworten, blockiere Drittanbieter und prüfe API-Aufrufe.

Ablauf

Die Reise hinter jedem erfolgreichen Test

Ein guter e2e-Test bereitet die Welt vor, führt eine Journey aus und hinterlässt die Umgebung so, wie er sie vorgefunden hat.

  1. 1

    Daten über die API bereitstellen

    Erstelle den Nutzer, den Plan und alle für die Journey benötigten Datensätze per API-Aufruf, nicht durch Klicken durch Setup-Screens.

  2. 2

    App öffnen

    Navigiere zur Einstiegsroute mit einer bereits bestehenden authentifizierten Session, damit der Test dort beginnt, wo der Nutzer startet.

  3. 3

    Aktion ausführen

    Steuere die Oberfläche über Role- und Label-Locators, einen Nutzerschritt nach dem anderen, ohne willkürliche Pausen einzulegen.

  4. 4

    Sichtbares Ergebnis prüfen

    Prüfe das Element, das der Nutzer sehen wollte. Wenn das Ergebnis ein Seiteneffekt ist, bestätige diesen zusätzlich über die API.

  5. 5

    Beweise bei Fehlern sichern

    Speichere Traces, Screenshots und Videos, damit ein roter Build in der CI ohne lokale Reproduktion diagnostiziert werden kann.

  6. 6

    Zustand zurücksetzen

    Lösche oder lass ablaufen, was der Test erstellt hat, oder führe jeden Test gegen isolierte Daten aus, sodass die Reihenfolge keine Rolle spielt.

Eine kurze Geschichte

Wie Browser-Testing erwachsen wurde

  1. 2006

    Selenium erscheint

    Browser-Automatisierung wird möglich und End-to-End-Testing wird zu einer echten Praxis mit einer steilen Lernkurve.

    06
  2. 2015

    Cypress wird gegründet

    Ein entwicklerfreundlicher Runner integriert den Browser in denselben Prozess und macht das Debugging wesentlich einfacher.

    15
  3. 2020

    Playwright erscheint

    Eine Cross-Browser-Library mit Auto-Waiting setzt neue Maßstäbe für die Stabilität von Browser-Tests.

    20
  4. 2021

    Playwright Test

    Ein First-Party-Runner ergänzt die Library um Fixtures, Parallelismus, Tracing und Sharding.

    21
  5. Heute

    Ein fokussiertes Quality Gate

    Teams halten e2e-Suites klein und wertvoll und lassen sie bei Pull Requests und vor dem Release laufen.

    Heute

Der vollständige Leitfaden

End-to-End Testing: Alles was Sie wissen müssen

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:

  1. getByRole — Buttons, Links, Überschriften und Inputs über die accessible role und den Namen.
  2. getByLabel — Formularsteuerelemente über ihr Label.
  3. getByText — Sichtbarer Text.
  4. getByPlaceholder, getByAltText, getByTitle — Andere sichtbare Attribute.
  5. 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/3 auf 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_URL aus; 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 storageState wiederverwenden.

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.

In der Praxis

Vier Bestandteile einer echten e2e-Suite

Eine Journey, ein Login-Shortcut, bereitgestellte Daten und der CI-Job, der sie ausführt.

tests/checkout.spec.ts
import { test, expect } from "@playwright/test";

test("a signed-in user can upgrade to Pro", async ({ page }) => {
  await page.goto("/pricing");

  await page.getByRole("button", { name: "Choose Pro" }).click();
  await expect(page).toHaveURL(/\/checkout/);

  await page.getByLabel("Card number").fill("4242 4242 4242 4242");
  await page.getByLabel("Expiry").fill("12/30");
  await page.getByLabel("CVC").fill("123");
  await page.getByRole("button", { name: "Pay" }).click();

  await expect(
    page.getByRole("heading", { name: "You're on Pro" }),
  ).toBeVisible();
});

Role-basierte Locators vs. CSS-Selektoren

Role- und Label-Locators beschreiben, was der Nutzer sieht, und schlagen fehl, wenn das Element nicht zugänglich ist. CSS-Selektoren brechen bei jedem Refactoring und testen stillschweigend das Markup statt des Verhaltens.

Bevorzugt
await page
  .getByRole("button", { name: "Add to cart" })
  .click();
Vermeiden
await page
  .locator("div.product > button.btn.btn--primary")
  .click();

Warten auf Bedingung vs. Sleep

Web-first Assertions wiederholen den Versuch, bis die Bedingung erfüllt ist. Ein fester Sleep ist entweder zu kurz und flaky oder zu lang und langsam, und er verschleiert die Race Condition, die er eigentlich überdecken sollte.

Bevorzugt
await expect(
  page.getByText("Order confirmed"),
).toBeVisible();
Vermeiden
await page.waitForTimeout(5000);
await expect(
  page.getByText("Order confirmed"),
).toBeVisible();

Abwägungen

Die ehrlichen Kosten von End-to-End-Tests

E2E-Tests sind die einzigen, die beweisen, dass ein Nutzer eine Aufgabe abschließen kann, aber sie sind auch die teuersten im Unterhalt. Setze sie gezielt ein.

Strengths

  • Sie testen das Nutzerverhalten

    Nur ein e2e-Test prüft die echte UI, API und Datenbank gemeinsam und findet so Verkabelungsfehler, die kein Unit-Test sehen kann.

  • Sie überleben Refactorings

    Da sie auf Rollen und sichtbarem Text basieren, bleiben Journey-Tests erfolgreich, während sich Komponenten, Frameworks und interne APIs ändern.

  • Sie sind eine gemeinsame Sprache

    Produktmanagement, QA und Engineering können dieselbe Spec lesen und sich darauf einigen, was 'funktioniert' bedeutet.

Trade-offs

  • Sie sind langsam und kostspielig

    Ein Browser-Test benötigt Sekunden und echte Infrastruktur; eine Suite aus Hunderten von Tests macht jeden Pull Request zur Kaffeepause.

  • Sie sind von Natur aus flaky

    Timing, Netzwerke, Animationen und gemeinsam genutzte Daten führen oft zu Fehlern, wenn Tests nicht sorgfältig isoliert werden und auf Zustände gewartet wird.

  • Sie liefern vage Fehlermeldungen

    Ein roter Journey-Test sagt dir nur, dass irgendetwas in einem langen Pfad kaputt ist. Die genaue Ursache zu finden, erfordert Traces und oft eine lokale Reproduktion.

Häufig gestellte Fragen

Häufig gestellte Fragen

Keep learning

Related topics from the roadmap.

$ Lernen Sie jetzt

Bereit, End-to-End Testing zu lernen?

Unser interaktives Tutorial führt Sie Schritt für Schritt durch End-to-End Testing — mit Quizzen und echtem Code, den Sie im Browser ausführen können.