Weniger Frust, mehr Codequalität

Wie GitHub Workflows schlechte Laune verhindern

Es gibt Pull Requests, bei denen der Code ganz ordentlich aussieht und trotzdem ein ungutes Gefühl bleibt.

Wurden die richtigen Tests geschrieben? Ist die Coverage noch hoch genug? Ist die Oberfläche zugänglich? Haben wir uns eine verwundbare Dependency eingefangen? GitHub Workflows lösen nicht alle Probleme. Aber sie nehmen einem Team die Frage ab, ob jemand zufällig an die immer gleichen Qualitätsprüfungen gedacht hat. Gute Workflows sichern nicht nur Reviews ab. Sie helfen schon während der Implementierung, Qualität sichtbar und überprüfbar zu machen. Ich mag daran vor allem den praktischen Effekt: Qualität wird weniger Heldentat und mehr Standardbetrieb. Niemand muss im Review raten, ob Tests, Coverage, Accessibility, Security und Dependencies geprüft wurden. Der Pull Request zeigt es.
Die Beispiele in diesem Artikel beziehen sich auf ein typisches Webprojekt mit PHP-Backend, Vue-3-TypeScript-Frontend und MySQL-Datenbank. Die Muster lassen sich aber genauso auf andere Stacks übertragen.

Wann Workflows laufen

Der erste sinnvolle Schritt ist nicht der perfekte Test, sondern eine klare Antwort auf die Frage: Wann soll überhaupt geprüft werden? Für mich gehören mindestens drei Trigger dazu: Pull Requests, Pushes nach protected main und ein manueller Start über workflow_dispatch. Pull Requests sichern die Änderung vor dem Merge ab. Pushes nach main prüfen, dass der geschützte Hauptbranch auch nach dem Merge sauber bleibt. Ein stark vereinfachtes Beispiel:

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]
  workflow_dispatch:

In einem Monorepo wird es schnell teuer, wenn jeder Workflow bei jeder Datei läuft. Deshalb sind paths-Filter so wertvoll. Backend-Checks müssen nicht laufen, wenn nur CSS in der Web-App geändert wurde. Web-Checks müssen nicht laufen, wenn nur eine Backend-Migration angepasst wurde.

on:
  pull_request:
    branches: [main]
    paths:
      - backend/**
      - .github/workflows/backend-tests.yml
  push:
    branches: [main]
    paths:
      - backend/**
      - .github/workflows/backend-tests.yml

Das ist keine Haarspalterei. Es macht den Unterschied zwischen einer Pipeline, die das Team akzeptiert, und einer Pipeline, die alle nur noch wegklicken wollen. Schnelle, relevante Checks werden ernst genommen.

Unit Tests sagen, ob die kleinen Zahnräder noch greifen

Unit Tests sind der schnelle Reality-Check für isolierte Logik. Sie sollten früh laufen, klar benannt sein und nicht in einem riesigen Testjob verschwinden. Wenn ein Parser, ein Preisrechner, ein Validator oder eine kleine Domänenfunktion kaputtgeht, will ich das nach Minuten wissen, nicht nach einem halben E2E-Marathon. Ein Workflow muss dafür nicht kompliziert sein:

jobs:
  backend-unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@<pinned-sha> # vX
      - run: composer install --no-interaction
      - run: composer test:unit
  web-unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@<pinned-sha> # vX
      - run: npm ci
      - run: npm run test:unit

Mir ist wichtig, dass Unit Tests als eigene Stufe sichtbar bleiben. Wenn sie fehlschlagen, ist die Botschaft meistens klar: Eine kleine Einheit verhält sich nicht mehr wie erwartet. Das ist ein anderes Signal als ein kaputter Browser- oder Integrationsflow.

Integrationstests sagen, ob die Teile zusammenpassen

Unit Tests können grün sein, während die Anwendung trotzdem falsch zusammengesetzt ist. Integrationstests schließen diese Lücke. Sie prüfen, ob Komponenten miteinander sprechen, ob API-Antworten richtig verarbeitet werden und ob UI-Zustände mit echten Daten noch funktionieren. Für eine Web-App können Integrationstests zum Beispiel mit Component Tests oder Browser-nahen Tests laufen:

{
  "scripts": {
    "test:integration": "playwright test -c playwright-ct.config.ts"
  }
}

Im Backend können Integrationstests API-Endpunkte, Datenbankinteraktion und Autorisierung prüfen. In der Web-App können sie Formulare, Fehlermeldungen, Ladezustände und Interaktion zwischen Komponenten abdecken. Das ist der Bereich, in dem viele echte Fehler sichtbar werden.

Coverage für Unit und Integration

Coverage ist kein Ersatz für gute Tests. Aber ohne Coverage-Gate merkt man oft erst spät, dass neue Logik kaum abgesichert wurde. Besonders hilfreich wird Coverage, wenn Unit- und Integrationstests zusammen betrachtet werden. Ein praktisches Muster ist eine dreistufige Coverage-Pipeline:

{
  "scripts": {
    "test:coverage": "npm run test:coverage:unit && npm run test:coverage:integration && npm run test:coverage:merge",
    "test:coverage:unit": "vitest run --coverage",
    "test:coverage:integration": "playwright test -c playwright-ct.config.ts",
    "test:coverage:merge": "node scripts/merge-coverage.mjs"
  }
}

Danach kann der Workflow Schwellen prüfen. Die konkreten Zahlen sind Teamentscheidung, aber als Startpunkt sind zum Beispiel 90 Prozent für Lines und Statements sowie 85 Prozent für Branches greifbar. Wichtig ist: Der Fail soll nicht nur meckern, sondern zeigen, wo man anfangen kann.

env:
  COVERAGE_LINES_THRESHOLD: 90
  COVERAGE_BRANCHES_THRESHOLD: 85
  COVERAGE_STATEMENTS_THRESHOLD: 90

Ich mag Coverage-Reports als HTML-Artefakt. Ein rotes Gate ohne Hinweis ist nur schlechte Laune mit Exit-Code. Ein rotes Gate mit Report und Fokusliste ist eine konkrete  Reparaturanleitung.

Die UI funktioniert, aber ist nicht zugänglich

Eine Oberfläche kann technisch funktionieren und trotzdem Menschen ausschließen. Deshalb gehört Accessibility, oft kurz A11y, nicht als freundlicher Nachsatz in die Definition of Done, sondern in die Tests. Für Web-UIs ist ein automatischer Check mit axe ein guter Anfang. Wir haben uns als Vorgabe gesetzt: Jeder Web-Integrationstest, der mit einer echten page arbeitet, prüft auch A11y. Dadurch wird Barrierefreiheit nicht zur späteren Erinnerung im Review, sondern zu einem normalen Teil jedes UI-nahen Tests.

import { AxeBuilder } from '@axe-core/playwright'
const results = await new AxeBuilder({ page }).analyze()
expect(results.violations).toEqual([])

Automatische Accessibility-Tests finden nicht alles. Sie ersetzen keine Tastaturprüfung, kein Nachdenken über Fokusführung und keine echte Nutzung mit assistiven Technologien. Aber sie verhindern, dass offensichtliche Probleme still in den Hauptbranch wandern.

E2E ist kein einzelner Befehl

End-to-End-Tests sind wertvoll, aber sie sind keine Magie. Ein guter E2E-Workflow beschreibt die ganze Strecke, nicht nur den letzten Testbefehl. Sonst wird aus einem roten Lauf schnell ein Ratespiel. In einem Monorepo ist das besonders angenehm, wenn die docker-compose.yml auf der Hauptebene liegt. Der Workflow kann dann aus dem Repository-Root heraus die gemeinsame Testumgebung starten: Datenbank, Backend, Web-App und alles, was der Browser-Test wirklich braucht. Die typische Reihenfolge sieht so aus:

steps:
  - uses: actions/checkout@<pinned-sha> # vX
  - run: npm ci
    working-directory: web
  - run: npx playwright install --with-deps chromium
    working-directory: web
  - run: docker compose up -d --build --wait database backend web
  - run: docker compose exec -T backend php artisan db:seed --class=E2eBaseSeeder --force
  - run: npm run test:e2e:ci
    working-directory: web
  - if: failure()
    run: docker compose logs --no-color database backend web
  - if: always()
    run: docker compose down -v

Die Datenbank wird dabei nicht irgendwie nebenbei vorbereitet. Wir nutzen einen eigenen E2E-Seeder, der einen kleinen, stabilen Datenbestand für die Browser-Tests erzeugt: Testnutzer, Berechtigungen, Beispielinhalte und genau die Zustände, die der Happy Path braucht. Wenn ein spezielles Szenario andere Daten braucht, wird direkt vor dieser Testsuite ein weiterer Szenario-Seeder ausgeführt.

steps:
  - run: docker compose exec -T backend php artisan db:seed --class=E2eBaseSeeder --force
  - run: npm run test:e2e:ci
    working-directory: web
  - run: docker compose exec -T backend php artisan db:seed --class=E2eSpecialCaseSeeder --force
  - run: npm run test:e2e:special-case
    working-directory: web

Diese Schrittfolge ist langweilig, und genau das ist gut. Checkout, Dependencies, Browser, App-Stack, Testdaten, Tests, Reports, Logs, Cleanup. Wer E2E so baut, bekommt reproduzierbare Fehler statt CI-Kaffeesatzlesen. Reports sind dabei keine Kür. Playwright-Reports, Screenshots, Videos oder Traces sind oft der Unterschied zwischen "läuft bei mir" und "ah, der Button liegt im mobilen Viewport unter dem Cookie-Banner". Je langsamer ein Test ist, desto besser muss seine Fehlermeldung sein.

Jetzt teilen: