gwoe-antragspruefer/docs/adr/0007-test-taxonomy.md
Dotty Dotter 7e20f910fe docs(#134): ADR 0007 — Test-Taxonomie
Phase 3 von #134: Klassifizierung Unit / Integration / E2E / Property / Smoke
mit Markern, Latenz-Budgets, Verzeichnis-Konventionen und Lauf-Befehlen.

Index aktualisiert (0007 zwischen 0006 und 0008 eingefuegt — ADRs sind
chronologisch, nicht numerisch sortiert).
2026-04-28 02:04:24 +02:00

5.9 KiB

0007 — Test-Taxonomie (Unit / Integration / E2E / Property / Smoke)

Status accepted
Datum 2026-04-28
Refs #50 (Umbrella E2E Functional Acceptance), #51-54 (Sub-A-D), #134 (Phase 3 Audit), ADR 0003

Kontext

Die Test-Suite ist organisch ueber drei Epochen gewachsen:

  1. Original Unit-Suite (#46, #91) — tests/conftest.py stubbt fitz/bs4/openai/pydantic_settings. Lokal in Sekunden lauffaehig, keine externen Calls, keine Live-Daten.
  2. E2E Functional Acceptance (#50 Umbrella) — tests/integration/ mit eigenem conftest.py, das die Stubs nicht setzt. HTTP gegen echte Landtags-Portale, PDF-Parsing mit echtem fitz, DB-Lookups gegen embeddings.db. Marker integration.
  3. Playwright UI-Tests (#120) — tests/e2e/test_ui.py, headless Chromium gegen die laufende App. Marker e2e.

Mit dem Backfill aus #134 kamen zusaetzlich:

  • Property-/Substring-Tests (ADR 0003) fuer LLM-Zitate gegen PDF-Seiten
  • Smoke-Tests (test_endpoints_smoke.py) — Endpoints nur auf Antwortcode + Format pruefen, kein Geschaeftslogik-Detail

Ohne klare Taxonomie weiss niemand, wo ein neuer Test hingehoert. Folge: ad-hoc Tests werden in tests/ abgelegt, Marker werden vergessen, und beim CI-Lauf brennen langsame Tests die schnellen mit ab.

Optionen

Option A — Flacher Test-Ordner ohne formale Kategorien

Status quo bis zu #50: alle Tests unter tests/, Marker frei waehlbar. Vorteile: keine Migrationskosten, niedrige kognitive Last. Nachteile: Kategorien implizit, Stub-Setup kollidiert mit echten Imports, Lauf-Dauer schwankt unvorhersehbar.

Option B — Drei harte Verzeichnisse (tests/unit/, tests/integration/, tests/e2e/)

Strenge raeumliche Trennung mit jeweils eigenem conftest.py. Vorteile: Stub-Konflikte ausgeschlossen, einfaches pytest tests/unit/. Nachteile: grosse Migration; viele bestehende Test-Files muessten in unit/ umziehen; verschachtelte Pfade werden vom Test-Runner und von Reports etwas sperriger.

Option C — Flacher Ordner + verbindliche Marker (gewaehlt)

tests/ flach, aber jede Datei traegt einen klaren Kategorie-Marker (integration, e2e, slow) oder ist Default-Unit. Neue Sub-Verzeichnisse nur wenn sie strukturell notwendig sind (z.B. tests/integration/ weil dort ein anderer conftest.py lebt — keine Stubs).

Vorteile: wenig Migrationsschmerz, Default-Run laeuft schnell, aber opt-in zu langsamen Suiten ist explizit (pytest -m integration). Nachteile: Disziplin noetig, Marker mssen gepflegt werden.

Entscheidung

Option C mit folgender expliziter Taxonomie:

Typ Marker Verzeichnis Latenz Was ist erlaubt
Unit (none, default) tests/*.py < 100 ms / Test Reines Python, alle externen Dependencies gestubbed in tests/conftest.py. Domain-Logik, Validatoren, Pure Functions, Datenstrukturen.
Smoke (none, default) tests/test_*_smoke.py < 200 ms / Test TestClient gegen app.main, nur Status-Code + Pflicht-Felder pruefen. Skipped wenn app.main nicht importierbar.
Property (none, default) tests/test_citations_substring.py u.a. < 500 ms / Test Invarianten-Checks gegen Fixture-Corpus. Substrings, Strukturmuster. PDF-Parsing erlaubt, aber nur gegen Fixtures im Repo.
Integration integration tests/integration/ < 5 s / Test, gesamt < 5 min Echtes HTTP gegen Landtags-Portale, echtes fitz gegen reale PDFs, DB-Lookups gegen embeddings.db. Eigenes conftest.py ohne Stubs. Opt-in via pytest -m integration.
E2E e2e tests/e2e/ < 30 s / Test Headless-Chromium gegen lokal laufende App oder Prod-URL. Tests koennen flaky sein — werden NICHT von Default-Run getriggert.
Slow slow (queruerend) beliebig Marker-Suffix zu jedem Typ. Ausschliessbar via pytest -m "not slow". Beispiel: ein Integration-Test, der pro BL einen Wahlprogramm-PDF herunterlaedt.

Lauf-Konvention (verbindlich, in pytest.ini definiert):

pytest                                  # Default — Unit + Smoke + Property, ~1s
pytest -m integration                   # nur E2E-Functional-Acceptance, ~5 min
pytest -m "integration and not slow"    # E2E ohne PDF-Downloads
pytest -m e2e                           # nur Playwright-UI-Tests
pytest -m ""  tests/                    # ALLES (auch lokal selten gebraucht)

Naming-Konvention:

  • test_<modul>.py — Unit-Tests fuer ein Modul
  • test_<feature>_smoke.py — Smoke-Tests
  • test_<feature>_substring.py / _substring_* — Property-Tests
  • Integration- und E2E-Tests heissen wie das Feature, das sie testen (z.B. test_adapters_live.py, test_ui.py).

Konsequenzen

Positiv

  • Default-Run bleibt schnell (< 2s) — niemand wartet bei jedem Save.
  • Klar, wo neue Tests landen: jeder neue Test im Default-Ordner ist ein Unit-Test mit Stubs; alles, was Live-HTTP/PDF/LLM braucht, geht zwingend nach tests/integration/.
  • CI kann Default-Suite als Pre-Commit-Gate nutzen, Integration-Suite nightly oder pre-deploy.

Negativ

  • Disziplin noetig: Marker vergessen → langsame Tests im Default-Run oder unbemerkte Lueckentest. Code-Review muss darauf achten.
  • Smoke-Tests sind technisch keine Unit-Tests (importieren app.main), aber wir behandeln sie wegen geringer Latenz als Default. Ausnahme bewusst akzeptiert.

Folgen fuer andere ADRs

  • ADR 0003 (Sub-D Citation-Property-Tests) bleibt gueltig; Property-Tests sind hier explizit als eigene Kategorie verortet.
  • Folge-Issue: Coverage-Baseline (.coveragerc mit fail_under pro Modul) — nicht im Skopus dieses ADRs, sondern eigenstaendig in Phase 3 von #134.
  • Folge-Arbeit: einzelne bestehende Test-Files umtaggen, falls sie faktisch Integration sind aber als Unit liefen (Audit ergab: keine bekannten Faelle, alle Live-Calls liegen in tests/integration/).