Ich bin Hossein

Senior Softwareentwickler

19. Juni 2026

Agentenbasierte End-to-End-Tests mit Playwright und Claude Hooks

End-to-End-Tests sind schon für sich allein nützlich. Noch wirkungsvoller werden sie, wenn sie Teil eines agentenbasierten Programmierkreislaufs sind.

Vor Kurzem habe ich Playwright zu RetroGo hinzugefügt. RetroGo ist ein Online-Werkzeug für Retrospektiven. Man braucht weder eine Registrierung noch eine Anmeldung. Man kann einen Raum erstellen, Karten hinzufügen, ähnliche Karten zusammenführen, abstimmen, die Ergebnisse besprechen und den Raum danach beenden.

Dieser Ablauf ist mir wichtig. Wenn ich etwas an der Raumlogik, der Abstimmungslogik oder am Frontend ändere, möchte ich wissen, ob der wichtigste Ablauf weiterhin funktioniert.

Das erste Ziel war, Tests für diesen Ablauf hinzuzufügen. Das zweite Ziel war, diese Tests für das Programmieren mit KI nutzbar zu machen.

Der Einstieg mit Playwright

Der erste Schritt war einfach: Playwright zum Projekt hinzufügen.

Playwright passt hier gut, weil es die Anwendung aus Sicht des Browsers testet. Es öffnet die Seite, klickt auf Schaltflächen, füllt Eingabefelder aus und prüft, was Nutzerinnen und Nutzer sehen können.

Genau das brauche ich für RetroGo. Das wichtige Verhalten liegt nicht nur in einer einzelnen Funktion oder einem einzelnen API-Endpunkt. Ich möchte den gesamten Ablauf testen:

  • Einen Raum erstellen
  • Karten hinzufügen
  • Karten zusammenführen
  • Zur Abstimmungsphase wechseln
  • Stimmen abgeben
  • Zum Besprechungsschritt wechseln
  • Den Raum beenden

Diese Art von Test gibt mir mehr Sicherheit, weil sie prüft, wie alle Teile zusammenspielen.

API und Frontend gemeinsam starten

RetroGo besteht aus einem API-Server und einem Frontend. Beides muss laufen, bevor der Browser für die Playwright-Tests startet.

Playwright unterstützt das mit webServer. Ich habe zwei Webserver in der Konfiguration eingetragen. Einer startet die Go-API, der andere das Frontend.

webServer: [
  {
    command: 'go run .',
    cwd: path.join(__dirname, 'api'),
    url: 'http://localhost:8080/health',
    name: 'API',
    timeout: 30 * 1000,
    reuseExistingServer: !process.env.CI,
    env: { ALLOWED_ORIGIN: 'http://localhost:4200' },
    stdout: 'pipe',
    stderr: 'pipe',
  },
  {
    command: 'npm start',
    cwd: path.join(__dirname, 'ui'),
    url: 'http://localhost:4200',
    name: 'Frontend',
    timeout: 90 * 1000,
    reuseExistingServer: !process.env.CI,
    stdout: 'pipe',
    stderr: 'pipe',
  },
],

Bis alles richtig funktionierte, waren mehrere Versuche nötig.

Das 404-Problem

Bei meinem ersten Versuch verwendete ich einen Raum-Endpunkt als URL für die Bereitschaftsprüfung:

url: 'http://localhost:8080/api/rooms/404NoRoom',

Die Idee war einfach: Wenn der Server läuft, verarbeitet er die Anfrage und liefert 404 zurück, weil dieser Raum nicht existiert.

Playwright betrachtet 404 jedoch nicht als erfolgreiche Bereitschaftsprüfung. Für webServer.url erwartet Playwright eine erfolgreiche Antwort, normalerweise 2xx oder 3xx. Eine 404-Antwort bedeutet zwar, dass der Server die Anfrage möglicherweise verarbeitet hat, für Playwright ist er damit aber noch nicht bereit.

Der Server lief also, doch Playwright wartete weiter, bis das Zeitlimit erreicht war:

[API] 2026/06/19 13:13:30 retro api listening on :8080
Error: Timed out waiting 30000ms from config.webServer.

Die Lösung war eine eigene Health-Check-URL, die 200 zurückgibt. Danach konnte Playwright sofort erkennen, dass die API bereit war.

Das Problem mit dem WebSocket-Ursprung

Das zweite Problem war weniger offensichtlich. API-Server und Frontend starteten ohne Fehler, und die Tests liefen an. Trotzdem konnte die Anwendung keine Verbindung aufbauen, weil der WebSocket-Handshake abgelehnt wurde.

Der Go-Server prüft den Origin-Header von WebSocket-Verbindungen. In Produktion ist https://retrogo.online der erlaubte Ursprung. Dieser Wert wird verwendet, wenn die Umgebungsvariable ALLOWED_ORIGIN nicht gesetzt ist.

Während der Playwright-Tests läuft das Frontend unter http://localhost:4200. Dieser Ursprung stimmt nicht mit retrogo.online überein, deshalb wurden alle WebSocket-Verbindungen abgelehnt.

Die Lösung war, die Umgebungsvariable über die Playwright-Konfiguration an den API-Server zu übergeben:

env: { ALLOWED_ORIGIN: 'http://localhost:4200' },

Das übersieht man leicht, weil der Server ohne Fehler startet. Das Problem tritt erst später auf, wenn die Anwendung eine Verbindung herstellen möchte.

Einfache Testbefehle hinzufügen

Ich habe zwei Skripte zu package.json hinzugefügt:

{
  "scripts": {
    "test": "npx playwright test",
    "test:ui": "npx playwright test --ui"
  }
}

Der normale Befehl test ist für das Terminal gedacht. test:ui öffnet die Playwright-Oberfläche. Sie ist hilfreich, wenn ich einen Test untersuchen oder den Ablauf Schritt für Schritt beobachten möchte.

Den echten RetroGo-Ablauf testen

Als die Umgebung bereit war, schrieb ich Tests für den vollständigen RetroGo-Ablauf.

Der Test erstellt einen Raum, fügt Karten hinzu, führt verwandte Karten zusammen, wechselt zur Abstimmung, gibt Stimmen ab, geht weiter zur Besprechung und beendet den Raum.

Das ist der ideale Ablauf, aber ein sehr wichtiger. Wenn er nicht funktioniert, ist das Produkt kaputt.

Für die Selektoren verwende ich getByRole und getByLabel statt CSS-Klassen. Das ist robuster, weil der Test prüft, was Nutzerinnen und Nutzer tatsächlich sehen, und nicht interne Details des HTML.

const wellSection = page.locator('section').filter({
  has: page.getByRole('heading', { name: 'What went well?' }),
});

await wellSection.getByLabel('What went well?').fill('Deploys were smooth');
await wellSection.getByRole('button', { name: /Add card/ }).click();
await expect(wellSection.getByRole('article')).toHaveCount(1);

Wenn ich eine CSS-Klasse umbenenne, läuft der Test weiterhin durch. Wenn ich die Schaltfläche entferne oder ihre Beschriftung ändere, schlägt er fehl. Genau so soll es sein.

Den Kreislauf agentenbasiert machen

Nachdem die Tests fertig waren, wollte ich sie in den KI-gestützten Programmierablauf einbauen.

Die Idee ist einfach:

Agent bearbeitet Code
Tests laufen automatisch
Agent liest den Fehler
Agent behebt das Problem
Tests laufen erneut

Das wichtigste Wort ist „liest“. Dieser Ablauf funktioniert nur, wenn die Testausgabe konkret genug ist, damit der Agent ohne Spekulationen weiß, was zu tun ist.

Wenn ein Playwright-Test fehlschlägt, liefert der list-Reporter eine Ausgabe wie diese:

  ok 1 - Room creator flow - Step 1: Create a room (1.2s)
  ok 2 - Room creator flow - Step 2: Join as facilitator (0.8s)
  x  3 - Room creator flow - Step 3: Add cards in the collecting phase (5.0s)

    Error: expect(received).toHaveCount(expected)

    Expected: 1
    Received: 0

    Locator: locator('section').filter(...).getByRole('article')

Diese Ausgabe ist direkt nutzbar. Der Agent weiß, welcher Schritt fehlgeschlagen ist, wie die Prüfung lautet und welcher Locator betroffen ist. Er kann den gerade geänderten Code ansehen und verstehen, warum die Anzahl 0 ist.

Ein allgemeiner Timeout ohne Kontext wäre ganz anders. Der Agent müsste raten, und solche Vermutungen sind oft falsch.

Der list-Reporter ist deshalb bewusst so konfiguriert:

// list liefert eine gut lesbare Terminalausgabe
reporter: [['list'], ['html', { open: 'never' }]],

Der HTML-Bericht wird weiterhin für Menschen erstellt. Der list-Reporter ist jedoch jener Teil, der im Terminal und in der Hook-Ausgabe erscheint, die der Agent liest.

Serverausgaben an den Agenten weitergeben

Für die Sichtbarkeit des Agenten ist noch ein Punkt wichtig: stdout: 'pipe' und stderr: 'pipe' bei beiden Webservern.

Wenn API oder Frontend nicht starten können, erscheint der Fehler in der Testausgabe mit dem Namen des Servers:

[API] api/server.go:15:2: undefined: MissingType
Error: Process from config.webServer exited early.

Ohne diese Einstellung sieht der Agent nur den Playwright-Fehler. Mit ihr sieht er die eigentliche Ursache: einen Go-Kompilierungsfehler, eine fehlende Umgebungsvariable oder einen Portkonflikt. All das erscheint direkt in der Ausgabe, die der Agent verarbeitet.

Das ist wichtiger, als es zunächst klingt. Wenn der Agent nicht erkennt, warum der Server nicht startet, verändert er möglicherweise Test- oder Anwendungscode, um das Problem zu umgehen. Mit der echten Ausgabe weiß er genau, was er beheben muss.

Einen Claude Hook hinzufügen

Um die Tests mit dem Programmierkreislauf zu verbinden, habe ich einen Hook hinzugefügt. Er läuft immer dann, wenn der Agent eine Datei bearbeitet oder schreibt:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "npm run test"
          }
        ]
      }
    ]
  }
}

Der Hook führt nach jeder Änderung npm run test aus. Die Testausgabe fließt als Kontext an den Agenten zurück. Dadurch muss man ihn nicht extra zum Testen auffordern. Er erhält das Ergebnis automatisch und kann damit den nächsten Schritt bestimmen.

Testanweisungen für den Agenten hinzufügen

Der Hook gibt dem Agenten Zugriff auf die Testergebnisse. Die Anweisungen erklären ihm, wie er sie verwenden soll.

## Testen

Verwende Playwright-Tests als wichtigste Rückmeldung für Änderungen, die Nutzerinnen und Nutzer betreffen.

Wenn du API, Frontend, Raumablauf, Abstimmungsablauf oder etwas anderes änderst, das die RetroGo-Nutzung beeinflussen kann, führe Folgendes aus:

```bash
npm run test
```

Die Playwright-Konfiguration startet vor den Tests sowohl die API als auch das Frontend.

Wenn Tests fehlschlagen:

- Lies die Terminalausgabe sorgfältig, bevor du weiteren Code bearbeitest.
- Verwende den Playwright-Fehler sowie stdout und stderr der Server als Rückmeldung.
- Behebe das eigentliche Problem, statt den Test zu verändern oder zu umgehen.
- Entferne keine Prüfungen, nur damit der Test erfolgreich ist.
- Überspringe keine Tests, außer die Nutzerin oder der Nutzer verlangt es ausdrücklich.
- Führe die Tests nach der Korrektur erneut aus.

Wenn der Fehler beim Serverstart entsteht:

- Prüfe die Ausgaben von API und Frontend in stdout und stderr.
- Suche nach Build-Fehlern, Portkonflikten oder fehlenden Umgebungsvariablen.
- Behebe zuerst das Startproblem und führe danach die Tests erneut aus.

Die wichtigste Regel steht jeweils am Ende: Nach einer Korrektur erneut testen. Der Kreislauf hilft nicht, wenn er nur einmal läuft.

Abschließende Gedanken

Agentenbasiertes Testen bedeutet für mich, einem Programmieragenten eine Rückmeldungsschleife mit genügend klaren Informationen zu geben.

Zuerst habe ich Playwright hinzugefügt. Danach habe ich Zeit in die richtige Umgebung investiert: in den Health-Check-Endpunkt und die WebSocket-Ursprungskonfiguration. Anschließend schrieb ich Tests mit rollenbasierten Selektoren. Zum Schluss verband ich alles über einen Hook mit Claude.

Die Einrichtung war aufwendig. Zwei Server verlässlich, in der richtigen Reihenfolge und mit der richtigen Umgebung zu starten, dauerte länger als das Schreiben der Tests. Dafür funktioniert npm run test jetzt als einzelner, verlässlicher Befehl. Nach jeder Codeänderung erfährt der Agent automatisch, ob der wichtigste Ablauf noch funktioniert.

So möchte ich KI in der Entwicklung einsetzen: nicht nur, indem ich sie bitte, Code zu schreiben, sondern indem ich ihr ein System gebe, das erkennt, wenn etwas kaputt ist.