Warum Actionability-Checks Flaky Tests verhindern
Wer E2E-Tests mit page.waitForTimeout oder festen Sleep-Aufrufen stabilisiert, bekämpft nur Symptome statt Ursachen. Playwright prüft vor jeder Aktion automatisch, ob ein Element sichtbar, stabil, aktiviert und tatsächlich klickbar ist, und wiederholt diese Prüfung, bis sie erfüllt ist oder ein Timeout erreicht wird. Dieser Artikel zeigt, wie diese Actionability-Checks funktionieren, warum feste Wartezeiten Flakiness verschleiern statt beheben und wie man Timeout-Fehler mit dem Trace Viewer gezielt diagnostiziert.
Inhaltsverzeichnis
- 1. Was Auto-Waiting ist und welches Problem es löst
- 2. Die Actionability-Checks im Detail
- 3. Warum waitForTimeout und sleep() fast immer falsch sind
- 4. Web-First Assertions statt manuellem Polling
- 5. Praxisbeispiel: Einen Flaky Test reparieren
- 6. Timeouts richtig konfigurieren
- 7. Robuste Locator-Strategien und Custom Assertions
- 8. Timeout-Fehler lesen und verstehen
- 9. Trace Viewer und Inspector im Einsatz
- 10. Zusammenfassung
- 11. FAQ
1. Was Auto-Waiting ist und welches Problem es löst
Auto-Waiting ist das Kernprinzip, das Playwright von älteren Test-Frameworks unterscheidet: Bevor eine Aktion wie click(), fill() oder check() ausgeführt wird, prüft Playwright automatisch, ob das Ziel-Element tatsächlich bereit für diese Aktion ist. Es gibt also keinen manuellen driver.wait(condition) Aufruf wie bei klassischem Selenium, bei dem Entwickler selbst festlegen müssen, worauf gewartet wird und wie lange. Playwright übernimmt diese Entscheidung selbst und wiederholt die Prüfung in kurzen Intervallen, bis sie erfüllt ist oder ein konfigurierbares Timeout erreicht wird.
Das Problem, das Auto-Waiting löst, ist so alt wie E2E-Testing selbst: Eine Webseite lädt Inhalte asynchron nach, Animationen verzögern die Interaktivität, und ein Server-Request braucht mal 100 Millisekunden, mal 1500. Ein Test, der sofort nach dem Laden der Seite klickt, trifft oft ein Element, das zwar im DOM existiert, aber visuell noch nicht bereit ist. Auto-Waiting verschiebt diese Verantwortung komplett vom Testautor auf das Framework, wodurch ganze Klassen von Timing-Fehlern gar nicht erst entstehen.
2. Die Actionability-Checks im Detail
Vor jeder Aktion führt Playwright eine feste Sequenz von Actionability-Checks aus. Ein Element muss im DOM angehängt sein (attached), sichtbar sein (visible, also eine von Null verschiedene Bounding Box ohne visibility: hidden), stabil sein (stable, also während mindestens zwei aufeinanderfolgenden Frames an derselben Position, was CSS-Transitions und Animationen berücksichtigt) und darf nicht durch ein anderes Element überdeckt werden (receives events). Für click() kommt zusätzlich enabled hinzu, für fill() zusätzlich editable.
Jeder dieser Checks wird nicht einmalig geprüft, sondern in einer Polling-Schleife wiederholt, typischerweise alle paar Millisekunden, bis alle Bedingungen gleichzeitig erfüllt sind. Erst dann führt Playwright die eigentliche Aktion aus. Schlägt ein einzelner Check dauerhaft fehl, etwa weil ein Modal-Overlay das Zielelement dauerhaft verdeckt, bricht Playwright nach Ablauf des actionTimeout mit einer präzisen Fehlermeldung ab, die genau benennt, welcher Check nicht erfüllt wurde.
3. Warum waitForTimeout und sleep() fast immer falsch sind
page.waitForTimeout() in Playwright oder cy.wait(ms) in Cypress pausieren die Testausführung für eine feste Anzahl Millisekunden, unabhängig davon, was auf der Seite tatsächlich passiert. Dieses Pattern ist fast immer die falsche Lösung, weil es zwei Fehlerarten gleichzeitig erzeugt: Ist die Wartezeit zu kurz, bleibt der Test flaky, weil die Bedingung manchmal noch nicht erfüllt ist. Ist sie zu lang, verschwendet jeder einzelne Testlauf Zeit, die sich über hunderte Tests in der CI-Pipeline zu Minuten summiert.
Der eigentliche Schaden ist subtiler: Ein fester Sleep behebt nicht die Ursache der Verzögerung, er verdeckt sie nur so lange, wie die reale Latenz unter dem gewählten Wert bleibt. Steigt die Serverlast, ändert sich ein Netzwerkpfad oder läuft die CI-Umgebung auf langsameren Runnern, reißt derselbe Sleep-Wert plötzlich und der Test wird flaky, ohne dass sich am Testcode etwas geändert hat. Ein Team mit vielen festen Sleeps in der Test-Suite bekämpft also Symptome in einer Endlosschleife, statt die zugrunde liegende Timing-Abhängigkeit einmalig sauber zu modellieren.
// bad-test.spec.js - Anti-Pattern: fester Sleep nach dem Klick auf "In den Warenkorb"
import { test, expect } from '@playwright/test';
test('add product to cart shows confirmation', async ({ page }) => {
await page.goto('/catalog/product/view/id/42');
await page.click('#product-addtocart-button');
// Anti-Pattern: feste Wartezeit, rät wie lange der AJAX-Call dauert
await page.waitForTimeout(2000);
const toast = page.locator('.message-success');
const text = await toast.textContent();
expect(text).toContain('You added');
});
4. Web-First Assertions statt manuellem Polling
Web-First Assertions wie expect(locator).toBeVisible() oder expect(locator).toHaveText() unterscheiden sich fundamental von klassischen Assertions auf einem einmalig ausgelesenen Wert. Statt den Text sofort mit textContent() abzurufen und zu vergleichen, wiederholt Playwright die zugrunde liegende Prüfung automatisch, bis sie entweder erfolgreich ist oder das expect-Timeout, standardmäßig 5 Sekunden, erreicht ist. Das Ergebnis: Der Test wartet exakt so lange wie nötig, nicht länger und nicht kürzer.
Der Unterschied zu einer manuellen Polling-Schleife, die zum Beispiel alle 500 Millisekunden isVisible() abfragt, liegt vor allem in der Robustheit und Lesbarkeit. Eigene Polling-Logik muss Sonderfälle wie Exceptions bei nicht existierenden Elementen, Race Conditions und Abbruchbedingungen selbst behandeln, was schnell zu inkonsistentem Code über verschiedene Testdateien hinweg führt. Web-First Assertions kapseln dieses Verhalten einmal korrekt im Framework, liefern bei Fehlschlag eine aussagekräftige Fehlermeldung mit dem letzten beobachteten Zustand und sind der De-facto-Standard für stabile Playwright-Tests.
// fixed-test.spec.js - Web-First Assertion wartet automatisch, bis der Toast erscheint
import { test, expect } from '@playwright/test';
test('add product to cart shows confirmation', async ({ page }) => {
await page.goto('/catalog/product/view/id/42');
await page.click('#product-addtocart-button');
// Kein fester Wait nötig: toBeVisible() pollt, bis die Assertion erfüllt ist
const toast = page.locator('.message-success');
await expect(toast).toBeVisible();
await expect(toast).toContainText('You added');
});
5. Praxisbeispiel: Einen Flaky Test reparieren
Ein typisches Beispiel aus der Praxis: Ein Add-to-Cart-Button löst einen AJAX-Request aus, der Warenkorb wird über Knockout- oder Alpine-Bindings aktualisiert, und danach erscheint eine Erfolgsmeldung. Der ursprüngliche Test aus dem ersten Codebeispiel nutzt page.waitForTimeout(2000), um auf diese Kette zu warten. Lokal auf einem schnellen Rechner lief der Test zuverlässig, in der CI-Pipeline mit mehreren parallelen Workern und geteilten Ressourcen schlug er sporadisch fehl, weil der Request gelegentlich über zwei Sekunden brauchte.
Die Lösung im zweiten Codebeispiel ersetzt den festen Sleep durch expect(toast).toBeVisible() gefolgt von expect(toast).toContainText(). Playwright wartet jetzt exakt so lange, bis die Erfolgsmeldung tatsächlich im DOM erscheint und lesbar ist, egal ob das 200 Millisekunden oder 3 Sekunden dauert. Der Test wurde dadurch nicht nur zuverlässiger, sondern im Median auch schneller, weil er nicht mehr unnötig die vollen zwei Sekunden wartet, wenn die Meldung längst da ist.
6. Timeouts richtig konfigurieren
Playwright kennt mehrere Timeout-Ebenen, die unabhängig voneinander konfiguriert werden. Der globale test.timeout begrenzt die Gesamtlaufzeit eines einzelnen Tests, standardmäßig 30 Sekunden. Der actionTimeout begrenzt, wie lange eine einzelne Aktion wie click() oder fill() auf erfüllte Actionability-Checks wartet. Der separate expect-Timeout, standardmäßig 5 Sekunden, gilt ausschließlich für Web-First Assertions und ist bewusst kürzer, weil eine fehlgeschlagene Assertion meist ein reales Problem anzeigt und nicht endlos maskiert werden soll.
In der Praxis ist es sinnvoller, diese Timeouts einmalig zentral in der playwright.config.js zu justieren, statt in einzelnen Tests wiederholt Sonderwerte zu setzen. Ein zu kurzer actionTimeout erzeugt False Positives auf langsamen CI-Runnern, ein zu langer versteckt echte Performance-Regressionen, weil Tests einfach länger warten, statt sichtbar fehlzuschlagen. Die Empfehlung: Timeouts so knapp wie möglich halten, damit ein Testfehler tatsächlich ein Signal für ein reales Problem bleibt, nicht nur eine willkürliche Zahl.
// playwright.config.js - Timeouts zentral justieren statt manuelle Waits einbauen
import { defineConfig } from '@playwright/test';
export default defineConfig({
timeout: 30_000, // Gesamt-Timeout pro Test
expect: {
timeout: 5_000, // Standard-Timeout für Web-First Assertions wie toBeVisible()
},
use: {
actionTimeout: 10_000, // Timeout für Actionability-Checks vor click(), fill(), etc.
navigationTimeout: 15_000, // Timeout für page.goto() und Navigationen
trace: 'retain-on-failure', // Trace nur bei fehlgeschlagenen Tests aufzeichnen
},
});
7. Robuste Locator-Strategien und Custom Assertions
Die Stabilität eines Locators beeinflusst direkt, wie zuverlässig die Actionability-Checks greifen. Locators wie getByRole() oder getByTestId() adressieren Elemente über stabile, semantische Attribute statt über CSS-Klassen oder DOM-Struktur, die sich bei jedem Frontend-Refactoring ändern können. Ein Locator, der bei jedem Build ins Leere greift, weil sich eine CSS-Klasse geändert hat, erzeugt Timeouts, die wie ein Actionability-Problem aussehen, tatsächlich aber ein reines Selektor-Problem sind.
Für Fälle, die über einfache Sichtbarkeits- oder Text-Assertions hinausgehen, etwa das Warten auf einen komplexen berechneten Zustand in einem Drittanbieter-Widget, bietet Playwright expect.poll() und toPass(). Beide wiederholen einen beliebigen Callback automatisch, bis er ohne Fehler durchläuft oder das Timeout erreicht ist, und übernehmen damit dieselbe Retry-Logik wie die eingebauten Web-First Assertions, nur für benutzerdefinierte Prüfungen. So bleibt auch komplexe Wartelogik deklarativ und muss nicht als manuelle while-Schleife nachgebaut werden.
// custom-locator.spec.js - flakiges Drittanbieter-Widget mit expect.poll() prüfen
import { test, expect } from '@playwright/test';
test('mini cart badge reflects item count', async ({ page }) => {
await page.goto('/');
const badge = page.getByTestId('minicart-item-count');
await page.getByRole('button', { name: 'Add to cart' }).click();
// Auto-retrying: wiederholt den Callback, bis er passt oder das Timeout erreicht ist
await expect.poll(async () => {
return await badge.textContent();
}, { message: 'minicart badge did not update', timeout: 10_000 }).toBe('1');
// Gleichwertige, idiomatischere Web-First Assertion für den einfachen Fall
await expect(badge).toHaveText('1');
});
8. Timeout-Fehler lesen und verstehen
Wenn eine Aktion nach Ablauf des actionTimeout fehlschlägt, gibt Playwright keine generische Timeout-Meldung aus, sondern protokolliert den Verlauf der Actionability-Checks im Detail. Die Fehlermeldung listet typischerweise auf: Der Locator wurde aufgelöst, das Element wurde gefunden, das Element ist sichtbar, das Element ist aber nicht stabil, weil eine CSS-Transition noch läuft, oder das Element wird von einem anderen Element überdeckt. Diese Reihenfolge zeigt exakt, an welchem Check die Aktion hängen geblieben ist.
Ein häufiges Muster in der Fehlermeldung ist element is not visible, obwohl das Element im DOM existiert. Das deutet fast immer auf display: none, eine display: none übergeordnete Elternstruktur oder eine Opacity von 0 hin. Ebenso häufig: element is outside of the viewport, was oft durch fehlendes Scrollen ausgelöst wird, das Playwright zwar automatisch versucht, aber nicht bei jeder Scroll-Container-Konstruktion zuverlässig auflösen kann. Das genaue Lesen dieser Fehlermeldung spart oft mehr Zeit als jedes Trial-and-Error mit zusätzlichen Waits.
9. Trace Viewer und Inspector im Einsatz
Der Trace Viewer macht die Actionability-Checks visuell nachvollziehbar. Mit trace: 'retain-on-failure' in der Konfiguration oder --trace on beim Testlauf zeichnet Playwright pro Test eine vollständige Timeline auf, inklusive DOM-Snapshots vor und nach jeder Aktion, Netzwerk-Requests und Konsolenausgaben. Nach einem fehlgeschlagenen Lauf öffnet npx playwright show-trace den Trace in einer interaktiven Oberfläche, in der jeder Schritt einzeln durchgeklickt werden kann.
Für die Entwicklung neuer Tests ist der Playwright Inspector, aktiviert über PWDEBUG=1 oder den --debug Flag, oft direkter: Er pausiert die Ausführung vor jeder Aktion und zeigt in Echtzeit, welcher Actionability-Check gerade aktiv geprüft wird. Beide Werkzeuge ersetzen das Trial-and-Error-Debugging mit zusätzlichen console.log-Aufrufen oder testweise eingefügten Sleeps durch eine präzise, reproduzierbare Diagnose. Die folgende Tabelle fasst zusammen, wie sich explizite Waits und Auto-Waiting über die relevanten Dimensionen unterscheiden.
# Suite mit aktiviertem Tracing ausführen, danach den Trace eines fehlgeschlagenen Tests öffnen
npx playwright test --trace on
# Einzelne Trace-Datei nach dem Lauf inspizieren
npx playwright show-trace test-results/checkout-add-to-cart/trace.zip
# Alternativ: Playwright Inspector für Schritt-für-Schritt-Debugging starten
PWDEBUG=1 npx playwright test checkout.spec.js
| Dimension | Explizite Waits (sleep/waitForTimeout) | Auto-Waiting / Web-First Assertions | Auswirkung |
|---|---|---|---|
| Zuverlässigkeit | Test bricht ab, wenn der Server langsamer ist als der feste Sleep | Wartet exakt so lange, bis die Bedingung erfüllt ist | Weniger Flaky Tests |
| Testlaufzeit | Immer die volle Sleep-Dauer, auch wenn das Element längst bereit ist | Endet sofort, sobald die Bedingung erfüllt ist | Kürzere CI-Laufzeiten |
| Wartungsaufwand | Sleep-Werte müssen bei jeder Performance-Änderung manuell nachjustiert werden | Kein Nachjustieren nötig, Timeout ist ein Sicherheitsnetz | Weniger technische Schulden |
| Fehlerdiagnose | Timeout meldet nur "2000ms vorbei", keine Ursache | Trace Viewer zeigt exakt, welcher Actionability-Check fehlschlägt | Schnelleres Debugging |
| Skalierbarkeit unter Last | Feste Werte reichen in CI oft nicht, lokal aber schon | Timeout skaliert unabhängig von der tatsächlichen Systemlast | Stabil in CI und lokal |
Mironsoft
E2E-Testing, Playwright-Setups und stabile CI-Pipelines für Magento- und Hyvä-Shops
Flaky Tests endgültig loswerden?
Wir analysieren eure bestehende Playwright- oder Cypress-Suite, ersetzen feste Sleeps durch Web-First Assertions und richten Trace-Viewer-Auswertung und Timeout-Konfiguration für stabile CI-Läufe ein.
Flaky-Test-Audit
Analyse bestehender Tests auf waitForTimeout, Sleeps und instabile Locators
Test-Refactoring
Feste Waits durch Web-First Assertions und expect.poll() ersetzen
CI-Integration
Timeout-Konfiguration und Trace-Auswertung in der Pipeline aufbauen
10. Zusammenfassung
Auto-Waiting ist der Grund, warum gut geschriebene Playwright-Tests ohne einen einzigen manuellen Sleep auskommen. Vor jeder Aktion prüft Playwright automatisch, ob ein Element angehängt, sichtbar, stabil, aktiviert und nicht überdeckt ist, und wiederholt diese Prüfung, bis sie erfüllt ist oder ein konfigurierbares Timeout erreicht wird. page.waitForTimeout() und vergleichbare feste Wartezeiten unterlaufen dieses Prinzip, weil sie weder auf reale Bedingungen reagieren noch die Ursache einer Verzögerung beheben, sondern sie nur so lange verdecken, wie die reale Latenz unter dem gewählten Wert bleibt.
Web-First Assertions wie toBeVisible() und toHaveText() übertragen dasselbe Retry-Prinzip auf Prüfungen und sind die richtige Wahl gegenüber manuellen Polling-Schleifen. Wenn ein Timeout dennoch auftritt, liefert die Fehlermeldung selbst schon die wichtigste Diagnose, und der Trace Viewer macht den Verlauf der Actionability-Checks vollständig sichtbar. Wer diese Werkzeuge konsequent nutzt, reduziert Flaky Tests strukturell, statt sie mit immer größeren Sleep-Werten notdürftig zu kaschieren.
Playwright Auto-Waiting - Das Wichtigste auf einen Blick
Actionability-Checks
Attached, visible, stable, enabled und receives events werden vor jeder Aktion automatisch geprüft und wiederholt.
waitForTimeout vermeiden
Feste Sleeps verdecken Timing-Probleme statt sie zu lösen und sind eine Hauptursache für Flaky Tests.
Web-First Assertions
toBeVisible()/toHaveText() retryen automatisch bis zum expect-Timeout, keine manuelle Polling-Schleife nötig.
Trace Viewer nutzen
trace: 'retain-on-failure' und show-trace zeigen exakt, welcher Check bei einem Timeout fehlschlug.