Flaky Tests gehören zu den größten Frustfaktoren in der Testautomatisierung. Ein häufiger Grund dafür sind feste Wartezeiten wie sleep() oder waitForTimeout().

In diesem Artikel zeige ich dir praxisnah, wie du in Playwright stabile, zustandsbasierte Warte-Strategien einsetzt – inklusive Negativbeispielen, Best Practices und fortgeschrittenen API-Waits. Das Ziel ist: Tests, die deterministisch laufen, verlässlich sind und leichter gewartet werden können.

Zeit vs. Zustand: Das Kernproblem Flaky Tests

Quelle: https://www.thequalityduck.co.uk/

In der Praxis zeigt sich immer wieder, dass instabile Tests nur selten durch fehlerhaften Code entstehen, sondern durch unzureichende Warte-Strategien. Besonders häufig betroffen sind Tests, die auf feste Zeitintervalle setzen, anstatt auf den tatsächlichen Zustand der Anwendung zu reagieren.

Dieses Problem tritt vor allem dann auf, wenn Tests lokal stabil laufen, im Continuous-Integration-Umfeld jedoch sporadisch fehlschlagen – obwohl sich am Code nichts geändert hat.

Um zu verstehen, warum das passiert und wie man es vermeidet, lohnt sich ein genauer Blick auf den Einsatz von waitForTimeout() und ähnlichen Mechanismen. Anhand eines typischen Szenarios wird im folgenden Abschnitt deutlich, warum zeitbasiertes Warten zu Flakiness führt und weshalb zustandsbasierte Strategien in Playwright die deutlich robustere Alternative sind.

Warum waitForTimeout() Tests flaky macht

Ein klassisches Szenario:

  • Lokal: Test läuft grün
  • CI: Test schlägt sporadisch fehl
  • Code wurde nicht geändert

Der Grund:
CI-Umgebungen sind langsamer, unvorhersehbarer und parallelisiert.

Feste Wartezeiten:

  • sind zu kurz → Test schlägt fehl
  • sind zu lang → Test wird langsam
  • prüfen keinen Zustand, sondern nur Zeit

❌ Anti-Pattern: Warten mit waitForTimeout()

Ein typischer Fehler beim Testen von Websites oder Webanwendungen ist das zeitbasierte Warten. Viele Testskripte setzen auf feste Intervalle, um Ladezeiten von Login-Formularen oder dynamischen Inhalten abzufangen.

Am folgenden Beispiel zeige ich anhand des Login-Formulars meiner WordPress-Website, wie es nicht umgesetzt werden sollte:

import { test, expect } from '@playwright/test';

test('WordPress Login zeigt Dashboard (flaky)', async ({ page }) => {
  await page.goto('https://walter-test-engineering.de/login.php');
  await page.fill('#user_login', 'admin');
  await page.fill('#user_pass', 'geheimespasswort');
  await page.click('#wp-submit');

  await page.waitForTimeout(3000); // ❌ instabil
  .....
});

Warum das problematisch ist:

Anti-Pattern und Best-Practice anhand des WordPress-Logins verdeutlicht
  • Die 3 Sekunden sind geraten – meine WordPress-Seite kann mal schneller oder langsamer laden.

  • API-Aufrufe oder das Rendern des Dashboards können länger dauern, z. B. durch Plugins oder langsame Serverantworten.

  • Der Test schlägt zufällig fehl – besonders im CI-Umfeld, wenn die Umgebung plötzlich eine andere ist.

  • Fehler lassen sich nur schwer reproduzieren.

>> Das ist kein echtes Warten auf den Zustand deiner WordPress-Seite – es ist schlichtes Hoffen, dass die Zeit reicht

Playwright-Best-Practice: Zustand statt Zeit

Playwright bringt Auto-Waiting mit.
Das bedeutet: Aktionen wie click() oder fill() warten automatisch, bis das Element sichtbar und klickbar ist.

Noch stabiler wird es mit expliziten Zustands-Checks.

✅ Stabiler Test mit sichtbarkeitsbasiertem Warten

Anstatt feste Wartezeiten zu verwenden, prüft dieser Test gezielt den tatsächlichen Zustand der WordPress-Seite – in diesem Fall, ob die Admin-Leiste nach dem Login sichtbar ist:

import { test, expect } from '@playwright/test';

test('WordPress Login zeigt Dashboard stabil', async ({ page }) => {
  await page.goto('https://walter-test-engineering.de/wp-login.php');
  await page.fill('#user_login', 'admin');
  await page.fill('#user_pass', 'geheimespasswort');
  await page.click('#wp-submit');

  await expect(page.locator('#wpadminbar')).toBeVisible(); 
  // ✅ Hier: wartet gezielt auf den Zustand, ohne vorheriges Warten
});

Warum dieser Ansatz stabil ist:

  • Der Test wartet aktiv darauf, dass die Admin-Leiste auf deiner WordPress-Seite sichtbar wird.
  • Es wird kein hart codiertes Timeout verwendet.
  • Dadurch läuft der Test schnell, deterministisch und zuverlässig – sowohl lokal als auch im CI.
  • Selbst, wenn Plugins oder langsame Server die Ladezeit verändern, schlägt der Test nicht mehr zufällig fehl.

Moderne Ansätze mit Playwright

Playwright setzt konsequent auf zustandsbasiertes Warten. Anstatt feste Wartezeiten oder manuelle Prüfungen einzubauen, wartet Playwright automatisch auf den richtigen Zustand eines Elements, bevor eine Aktion ausgeführt wird.

Kompakte Variante

await page.getByRole('button', { name: 'Login' }).click();

Dieser einzelne Befehl wartet automatisch darauf, dass der Button:

  • im DOM vorhanden ist
  • sichtbar ist
  • aktiviert und klickbar ist

Erst, wenn alle Bedingungen erfüllt sind, wird der Klick ausgeführt.

Eine weitere, gut lesbare Variante

const button = page.getByRole('button', { name: 'Login' });

await expect(button).toBeVisible();
await expect(button).toBeEnabled();

await button.click();

Diese Variante ist besonders geeignet, wenn:

  • der gewünschte Zustand bewusst dokumentiert werden soll
  • Tests als lebende Spezifikation dienen
  • Lesbarkeit wichtiger ist als maximale Kürze

Warum diese Ansätze stabil sind

  • Kein zeitbasiertes Warten
  • Automatische Retries bei kurzzeitigen UI-Zuständen
  • Deterministisches Verhalten
  • Deutlich weniger Flakiness

Ein fortgeschrittenes Beispiel: Warten auf API + UI

Bei dynamischen Inhalten reicht es oft nicht aus, nur auf DOM-Elemente zu warten. AJAX-Aufrufe laden Daten asynchron, und nur auf die UI zu warten, kann flaky Tests verursachen. Nachfolgend möchte ich beispielhaft nach Blog-Beiträgen zur „Testautomatisierung“ auf einer Website suchen.

❌  Negativbeispiel


await page.fill('#search-input', 'Testautomatisierung');
await page.click('#search-submit');

// Prüft direkt die UI, ohne auf die API zu warten
await expect(page.locator('.post')).toHaveCountGreaterThan(0);

Problem:

  • WordPress REST-API-Aufruf (/wp-json/wp/v2/posts) kann länger dauern

  • Liste der Suchergebnisse ist möglicherweise noch leer

  • Test schlägt zufällig fehl → flakig und schwer reproduzierbar

✅ Best Practice: API-Wait + UI-Check


await page.fill('#search-input', 'Testautomatisierung');
await page.click('#search-submit');

// Warten, bis die WordPress REST-API die Beiträge liefert
await page.waitForResponse(response =>
  response.url().includes('/wp-json/wp/v2/posts') &&
  response.status() === 200
);

// Danach die UI prüfen
await expect(page.locator('.post')).toHaveCountGreaterThan(0);

Warum diese Strategie robust ist:

  • Test wartet, bis die API tatsächlich Beiträge liefert

  • UI wird erst geprüft, wenn die Inhalte vorhanden sind

  • Test ist deterministisch, zuverlässig und robust – auch bei langsamen Servern oder Netzwerkverzögerungen

Typische Fehler bei Warte-Strategien

- waitForTimeout() in Produktivtests
– Warten auf falsche API-Endpunkte
– Nur auf DOM warten, obwohl Daten asynchron geladen werden
– Zustände nicht explizit prüfen

Cheat-Sheet: Stabile Warte-Strategien in Playwright

Strategie Playwright-Befehl Wann einsetzen
Auto-Waiting click(), fill() Standard
Sichtbarkeit expect(locator).toBeVisible() UI-Zustand
Aktiviert expect(locator).toBeEnabled() Buttons / Aktionen
Text / Count toHaveText(), toHaveCountGreaterThan() Inhalte prüfen
Netzwerk waitForResponse() API-abhängige Views
Load-State waitForLoadState() Navigation
Timeout waitForTimeout() Nur Debug / Demo

Fazit

Stabile Playwright-Tests entstehen nicht durch längere Wartezeiten, sondern durch sauberes Warten auf Zustände. Wer feste Timeouts vermeidet und stattdessen gezielt prüft, wann ein Element sichtbar, aktiv oder inhaltlich korrekt ist, reduziert Flakiness erheblich.

Besonders wirkungsvoll wird das, wenn UI-Checks mit API- oder Netzwerk-Waits kombiniert werden, sodass Tests erst dann weitermachen, wenn die zugrunde liegenden Daten tatsächlich geladen sind.

Playwright unterstützt diesen Ansatz von Haus aus mit Auto-Waiting, das viele klassische Selenium-Probleme bereits löst – vorausgesetzt, man nutzt es bewusst und ergänzt es dort, wo explizite Zustandsprüfungen sinnvoll sind. Das Ergebnis sind Tests, die nicht nur stabiler, sondern auch schneller, CI-tauglich und langfristig leichter wartbar sind.

Abschließend möchte ich Dir mitgeben:

Wer auf Zeit wartet, hofft.
Wer auf Zustände wartet, testet zuverlässig. : )

Weiterführende Links

Beitrag verfasst von:

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Bitte füllen Sie dieses Feld aus.
Bitte füllen Sie dieses Feld aus.
Bitte gib eine gültige E-Mail-Adresse ein.
Sie müssen den Bedingungen zustimmen, um fortzufahren.