Ihr pusht eine kleine Änderung, die Pipeline läuft, und ein Test schlägt fehl. Ihr startet ihn ohne jede Codeänderung neu, und er läuft durch. An eurer App hat sich zwischen diesen beiden Läufen nichts geändert. Das ist ein flaky Test, und wer ernsthaft Zeit mit Playwright verbracht hat, kennt dieses Gefühl in der Magengrube, wenn so etwas auftaucht.

Was Junior-QA-Engineers niemand sagt: Flaky hat meistens nichts damit zu tun, dass die App kaputt ist. Es bedeutet, dass ein Test eine Annahme über Timing getroffen hat, die nur manchmal stimmt.

Es liegt selten an der App

Wenn ein Test sporadisch fehlschlägt, ist der Reflex, das Feature verantwortlich zu machen. In neun von zehn Fällen ist das eigentliche Problem, dass der Test auf einen Button klickt, bevor der tatsächlich klickbar ist, oder einen Text prüft, bevor der API-Call, der ihn erzeugt, abgeschlossen ist. Die App hat einwandfrei funktioniert. Der Test hat nur nicht gewartet, bis sie fertig war.

Diese Unterscheidung ist wichtig, weil sie ändert, was ihr repariert. Einem Phantom-Bug in Anwendungscode hinterherzujagen, der gar nicht kaputt ist, verbrennt einen ganzen Nachmittag für nichts.

Die üblichen Verdächtigen

Ein paar Muster tauchen immer wieder auf, sobald man flaky Tests bis zu ihrer eigentlichen Ursache zurückverfolgt:

  • Race Conditions: Der Test interagiert mit einem Element, bevor die Seite es fertig gerendert hat oder bevor ein Netzwerk-Request abgeschlossen ist. Das ist der große Brocken, locker 70 % dessen, was mir begegnet.
  • Selektoren, die am Styling hängen: Ein Test, der sich .btn-primary-v2 greift, bricht in dem Moment, in dem ein Entwickler eine Klasse umbenennt. Am Feature hat sich nichts geändert, am Test trotzdem.
  • Geteilter Zustand zwischen Tests: Test B geht davon aus, dass die Datenbank in dem Zustand ist, in dem Test A sie hinterlassen hat. Führt man sie in anderer Reihenfolge oder parallel aus, schlägt Test B aus Gründen fehl, die mit Test B nichts zu tun haben.
  • Umgebungsrauschen: Langsamere CI-Runner, kalte Caches, Third-Party-APIs, die einen schlechten Tag haben. Das gibt es wirklich, aber es ist auch die Ausrede, zu der man greift, wenn die eigentliche Ursache eine der drei oben ist.

Repariert zuerst die Waits

Playwright hat den Großteil des Timing-Problems bereits für euch gelöst, und viele flaky Suites nutzen genau diesen Teil nicht. Der klassische Fehler:

await page.click('.submit-button');
await page.waitForTimeout(3000);
expect(await page.textContent('.success-message')).toBe('Order confirmed');

Dieses waitForTimeout(3000) ist geraten. Manchmal reichen 3 Sekunden locker, manchmal ist der Server an dem Tag langsam und sie reichen nicht, und so oder so verschwendet ihr entweder Zeit oder rennt weiter gegen die Seite an. Playwrights Locators warten automatisch für euch, also erledigt das hier dieselbe Aufgabe, aber richtig:

await page.getByRole('button', { name: 'Submit' }).click();
await expect(page.getByText('Order confirmed')).toBeVisible();

expect(locator).toBeVisible() fragt die Seite so lange ab, bis das Element erscheint oder ein Timeout erreicht wird, ohne willkürlichen Sleep und ohne Raterei. Ich würde schätzen, dass die Hälfte der flaky Tests, die ich im letzten Jahr debuggt habe, darauf hinauslief, ein waitForTimeout gegen eine echte Assertion wie diese zu tauschen.

Isoliert eure Testdaten

Wenn euer Login-Test und euer Checkout-Test beide auf test@example.com zugreifen, habt ihr eine Abhängigkeit zwischen zwei Tests gebaut, die einander gar nicht kennen sollten. Die Warenkorb-Reste des einen Tests ruinieren still und leise den anderen.

Die Lösung ist unspektakulär: pro Lauf eindeutige Testdaten generieren. Ein Timestamp in der E-Mail-Adresse, ein frischer User im beforeEach, was immer zu eurem Setup passt. Das ist erstmal mehr Code. Es bedeutet aber auch, dass ihr eure Suite parallel laufen lassen könnt, ohne dass sich die Tests gegenseitig auf die Füße treten, was bei einem CI-Budget mehr zählt, als es klingt.

Retries sind ein Pflaster, keine Heilung

Playwright lässt euch Retries in der CI konfigurieren, und ich nutze sie, aber ich will ehrlich sein, was sie tatsächlich tun: Sie verstecken das Problem, sie beheben es nicht. Ein Test, der einmal fehlschlägt und beim Retry durchläuft, ist immer noch ein flaky Test. Er ist nur nicht laut genug gescheitert, damit es jemandem auffällt.

Ich lasse Retries in der CI trotzdem an, weil eine rote Pipeline, die wegen eines echten Timing-Schluckaufs ein Deployment blockiert, ihre eigene Art von Verschwendung ist. Aber ich tracke, wie oft jeder Test einen Retry braucht, um durchzulaufen. Ein Test, der ständig Retries braucht, wird markiert und neu geschrieben, nicht in Ruhe gelassen, weil er „irgendwann grün wird".

Was euch das am Ende bringt

Nichts davon ist kompliziert, wenn man es einmal gesehen hat, aber ich habe länger gebraucht, als ich zugeben möchte, um flaky Tests nicht mehr als Hintergrundrauschen zu behandeln. Eine Test-Suite, der niemand vertraut, ist schlimmer als gar keine, denn ohne Tests hat wenigstens niemand falsche Sicherheit. In dem Moment, in dem jemand im Team sagt „ach, der Test schlägt manchmal fehl, ignorier ihn", habt ihr das verloren, was Tests euch eigentlich geben sollen.

Repariert die Waits, isoliert die Daten, und behandelt Retries als Signal statt als Lösung. Glamourös ist diese Arbeit nicht, aber sie macht den Unterschied zwischen einer Test-Suite, der ihr vertraut, und einer, um die ihr herumarbeitet.