Zuverlässige Playwright-Tests - Browserfehler sollen sich selbst erklären
Ein Browser-Test besteht auf einem Laptop, schlägt in der Continuous Integration fehl und besteht beim nächsten Versuch wieder. Ist die Anwendung fehlerhaft, ist der Test zu ungeduldig oder war ein externer Dienst kurzzeitig nicht erreichbar? Eine längere Wartezeit kann das rote Ergebnis verschwinden lassen, beantwortet diese Frage aber nicht.
Zuverlässige Browser-Automatisierung bedeutet weniger, jeden Durchlauf grün zu machen, als vielmehr jeden Fehler aussagekräftig zu machen. Playwright stellt dafür Locators, Actionability-Prüfungen, wiederholte Assertions, isolierte Browser-Kontexte und Traces bereit. Keine dieser Funktionen kann jedoch einen Test mit einer falschen Erwartung oder unkontrollierten Daten retten. Der Test benötigt weiterhin einen klaren Vertrag mit der Benutzeroberfläche.
Dieser Leitfaden untersucht diesen Vertrag für kleine Webprojekte. Die Beispiele verwenden Playwright Test mit Node.js, sind aber illustrativ und kein Ergebnisbericht zu einer bestimmten Anwendung.
Zuerst feststellen, was tatsächlich instabil ist
"Flaky" wird oft als Sammelbegriff für mehrere unterschiedliche Probleme verwendet. Ihre Trennung verändert die angemessene Reaktion:
- Produktfehler: Ein Button wird manchmal von einem Overlay verdeckt, ein Request konkurriert zeitlich mit der Navigation oder gespeicherte Daten sind tatsächlich inkonsistent.
- Testfehler: Das Skript wählt den dritten Button aus, nur weil er gerade an dritter Stelle steht, prüft einen Zustand vor der Aktualisierung der UI oder hängt von einem vorherigen Test ab.
- Umgebungsfehler: Dem Testserver gehen Ressourcen aus, ein Browser-Prozess stürzt ab oder DNS ist nicht verfügbar.
- Unkontrollierte Abhängigkeit: Der Test wartet auf eine fremde Seite, API, Einblendung oder ein Konto, dessen Zustand das Projekt nicht bestimmen kann.
Eine pauschale Erhöhung des Timeouts behandelt alle vier Fälle als "zu langsam". Das ist bequem, liefert aber schwache Evidenz. Eine nützlichere erste Frage ist enger gefasst: Welche beobachtbare Bedingung erwartete der Test, und welche Evidenz erklärt, warum sie nicht vorhanden war?
Locators als Verträge der Benutzeroberfläche wählen
Die Locator-Dokumentation von Playwright empfiehlt benutzerorientierte Attribute und ausdrückliche Verträge. Ein Locator wird ausgewertet, wenn eine Aktion oder Assertion ihn verwendet. Dadurch kann er nach einem erneuten Rendern das aktuelle Element auflösen, statt an einem früheren DOM-Knoten festzuhalten.
Bei einem gewöhnlichen Formular kann der Test dieselben Begriffe wie ein Leser verwenden:
await page.getByLabel('Email address').fill('[email protected]');
await page.getByRole('button', { name: 'Subscribe' }).click();
await expect(page.getByRole('status')).toHaveText('Subscription saved');
Der Button-Locator verwendet seine Rolle und seinen Accessible Name. Die W3C-Leitlinien zu Accessible Names erklären, dass ein Name den Zweck eines Elements vermittelt und es von ähnlichen Elementen unterscheidet. Sie empfehlen außerdem, nach Möglichkeit sichtbaren Text und native HTML-Techniken zur Benennung zu verwenden. Damit ist ein Role Locator mehr als nur Syntax: Wenn sich die sichtbare Beschriftung ändert, bemerkt der Test wahrscheinlich ebenfalls eine für Benutzer sichtbare Änderung.
Dadurch wird ein rollenbasierter Test ausdrücklich nicht zu einem Accessibility Audit. Er prüft lediglich einen Teil der von der Seite bereitgestellten Accessibility-Semantik.
Wenn eine Test-ID der klarere Vertrag ist
Benutzerorientierte Locators sind nicht immer eindeutig. Eine übersetzte Oberfläche kann bewusst unterschiedliche Beschriftungen verwenden, ein reines Icon-Steuerelement muss sorgfältig benannt werden und zwei gültige Steuerelemente können denselben Text tragen. Playwright beschreibt Test-IDs als robuste, ausdrückliche Verträge, weist aber darauf hin, dass sie nicht benutzerorientiert sind.
<button data-testid="save-profile">Save</button>
await page.getByTestId('save-profile').click();
Der Trade-off sollte ausdrücklich benannt werden. Eine Test-ID kann bei Änderungen an Text und Layout stabil bleiben. Das ist hilfreich, wenn diese Änderungen für das Szenario irrelevant sind. Umgekehrt deckt sie nicht auf, dass die sichtbare Beschriftung irreführend geworden ist. Es gibt keinen universellen Gewinner; der Locator sollte zu dem Verhalten passen, das der Test schützen soll.
Lange CSS- oder XPath-Ketten sind meistens ein schlechterer Vertrag, weil sie Container, Klassen und Positionen abbilden, die Benutzer nicht interessieren. Methoden wie nth() können nach einer Änderung der Liste zudem unbemerkt auf ein anderes Element zeigen. Sie stehen für ungewöhnliche Fälle zur Verfügung, doch Bequemlichkeit sollte nicht mit Identität verwechselt werden.
Actionability statt zeitlicher Vermutungen nutzen
Vor einem normalen click() auf einen Locator führt Playwright dokumentierte Actionability-Prüfungen aus. Es wartet darauf, dass der Locator genau ein Element auflöst und dieses Element sichtbar, stabil, aktiviert und in der Lage ist, Events zu empfangen. Werden diese Bedingungen vor dem jeweiligen Timeout nicht erfüllt, schlägt die Aktion fehl.
Dieses Verhalten macht eine feste Pause wie waitForTimeout(2000) zu einer schlechten Standardeinstellung. Zwei Sekunden können in einem Durchlauf unnötig lang und in einem anderen trotzdem zu kurz sein. Noch wichtiger ist, dass das Verstreichen von Zeit selten die fachliche Bedingung ist, die der Test benötigt.
Das Warten auf einen Response, eine URL, den Zustand eines Buttons, eine Statusmeldung oder ein anderes beobachtbares Ergebnis vermittelt den Grund für das Warten. Außerdem kann der Vorgang enden, sobald die Bedingung erfüllt ist.
Die Option force verdient ähnliche Vorsicht. Sie deaktiviert nicht wesentliche Actionability-Prüfungen für unterstützte Aktionen. Das kann bei einer bewusst ungewöhnlichen Interaktion helfen. Ein beiläufig erzwungener Click kann jedoch verbergen, dass ein echter Benutzer das Ziel nicht anklicken konnte, weil ein anderes Element darüberlag.
Das Ergebnis prüfen, nicht den Augenblick
Aktionen und Assertions behandeln unterschiedliche Hälften eines Szenarios. Ein Click beschreibt, was der Benutzer getan hat; eine Assertion legt fest, was die Anwendung schließlich anzeigen muss. Die webspezifischen asynchronen Assertions von Playwright prüfen ihre Bedingung wiederholt, bis sie erfüllt ist oder das Assertion-Timeout erreicht wird.
// Timing-sensitive snapshot: it checks only the current moment.
expect(await page.getByText('Subscription saved').isVisible()).toBe(true);
// Web-first assertion: it waits for the observable outcome.
await expect(page.getByText('Subscription saved')).toBeVisible();
Die zweite Form schwächt die Erwartung nicht ab. Sie schlägt weiterhin fehl, wenn die Meldung nie erscheint. Sie berücksichtigt lediglich, dass Browser und vernetzte Anwendungen asynchron aktualisiert werden. Auch ein fehlendes await ist bedeutsam, da wiederholte Assertions asynchron sind.
Timeouts bleiben als Grenzen notwendig. Sie verhindern, dass ein Test unbegrenzt wartet. Ein höheres Timeout ist für sich genommen jedoch keine Synchronisierungsstrategie. Benötigt eine Bedingung regelmäßig erheblich mehr Zeit als erwartet, sollte vor einer Erhöhung der Grenze geprüft werden, ob die Anwendung, die Umgebung oder die Erwartung dafür verantwortlich ist.
Zustand isolieren und die Grenze kontrollieren
Ein robuster Locator kann verunreinigte Daten nicht korrigieren. Der Best-Practices-Leitfaden von Playwright empfiehlt, Tests möglichst zu isolieren und unabhängige Cookies, Local Storage, Session Storage und Daten zu verwenden. Isolation verhindert, dass ein Fehler nachfolgende Tests rot färbt, und erleichtert die Reproduktion eines einzelnen Szenarios.
Dafür muss nicht die gesamte Anwendung für jede Assertion neu aufgebaut werden. Ein kontrolliertes Setup kann genau das Konto oder den Datensatz erstellen, den ein Szenario benötigt. Entscheidend ist die Zuständigkeit: Ein Test sollte nicht davon abhängen, dass ein anderer Test zuerst ausgeführt wurde.
Dasselbe Prinzip gilt an Netzwerkgrenzen. Wenn ein Checkout-Test die projekteigene Bestätigungsseite prüfen soll, kann eine live angebundene Payment Sandbox eines Drittanbieters Variabilität hinzufügen, die mit dieser Seite nichts zu tun hat. Playwright empfiehlt, Abhängigkeiten von Drittanbietern nicht zu testen und die erwartete Antwort nach Möglichkeit zu kontrollieren. Ein separater Integrationstest kann den externen Vertrag in einer angemessenen Häufigkeit prüfen.
Es gibt ein Gegenargument: Zu viel Mocking kann einen vollkommen stabilen Test für ein imaginäres System erzeugen. Diese Sorge ist berechtigt. Die hilfreiche Unterscheidung lautet nicht "alles mocken" gegen "nichts mocken", sondern welche Grenze jeder Test abzudecken beansprucht. Eine kleine Suite kann schnelle, kontrollierte UI-Tests mit weniger häufigen End-to-End-Integrationsprüfungen kombinieren.
Retries zum Sammeln von Evidenz verwenden, nicht zum Löschen von Fehlern
Ein Retry kann zeigen, ob sich ein Fehler wiederholt, und umfangreichere Diagnosedaten auslösen. Ein erfolgreicher Retry beweist nicht, dass der erste Fehler harmlos war. Wird er ohne Untersuchung automatisch akzeptiert, können sporadische Produktfehler zu Hintergrundrauschen werden.
Der Trace Viewer von Playwright kann je nach Trace-Konfiguration Aktionen, DOM-Snapshots, Quellcodepositionen, Logs, Konsolenmeldungen und Netzwerkanfragen anzeigen. Die offizielle Dokumentation führt on-first-retry als CI-Option und retain-on-failure als Alternative ohne aktivierte Retries auf:
import { defineConfig } from '@playwright/test';
export default defineConfig({
retries: 1,
use: {
trace: 'on-first-retry',
},
});
Es ist möglich, jeden Trace aufzuzeichnen, doch die Dokumentation warnt vor dem hohen Performance-Aufwand. Die passende Aufbewahrungsrichtlinie hängt von der Größe der Suite, dem CI-Speicher, dem Datenschutz und den Debugging-Anforderungen ab. Traces können Seiteninhalte und Netzwerkdetails enthalten. Daher verdienen Zugriff und Aufbewahrung dieselbe Sorgfalt wie andere Test-Artefakte.
Eine praktische Fehleranalyse
Bei einem sporadischen Fehler kann eine kurze Analyse informativer sein als eine sofortige Änderung des Timeouts:
- Den ersten Fehler lesen und das erwartete, benutzersichtbare Ergebnis bestimmen.
- Prüfen, ob der Locator das beabsichtigte Element eindeutig beschreibt.
- Den Trace rund um die erste fehlgeschlagene Aktion untersuchen, nicht nur den letzten Screenshot.
- Nach Overlays, ausstehenden Requests, Konsolenfehlern, unerwarteten Weiterleitungen und veränderten Daten suchen.
- Den fehlgeschlagenen Test einzeln ausführen, um Abhängigkeiten von gemeinsamem Zustand sichtbar zu machen.
- Die Ursache klassifizieren, bevor Anwendungscode, Setup, Locator, Assertion, Umgebung oder Timeout geändert werden.
Zuverlässigkeit ist Klarheit trotz Variation
Keine Selector-Strategie kann eine fehlerfreie Suite garantieren. Benutzeroberflächen ändern sich, Umgebungen fallen aus und manches Verhalten verteilter Systeme ist tatsächlich nicht deterministisch. Die Waiting- und Retry-Funktionen von Playwright reduzieren vermeidbare Timing-Races, können aber nicht entscheiden, was das Produkt tun sollte.
Ein zuverlässiger Test verfolgt daher ein bescheidenes Ziel: Er beschreibt ein sinnvolles Verhalten, kontrolliert den benötigten Zustand, wartet auf beobachtbare Bedingungen und hinterlässt genügend Evidenz, wenn die Realität abweicht. Die Frage nach einem Fehler sollte nicht nur lauten: "Wie bekommen wir diesen Test grün?" Sie sollte lauten: "Was lehrt dieses Ergebnis über die Benutzeroberfläche oder den Test?"
