Teststrategi — hva testes hvor⚓︎
Kai v2 (Kai.slnx) verifiseres på tre nivåer. Målet er en rask og stabil testsuite vi kan
stole på i CI: unit-tester bærer hovedtyngden, integrasjonstester dekker persistens og adaptere mot
ekte teknologi, og et lite E2E-sett verifiserer at hele worker-pipelinen henger sammen.
Testpyramiden⚓︎
flowchart TD
E2E["E2E — Enova.Kai.E2ETests<br/>hele pipelinen via Aspire + WireMock"]
INT["Integrasjon — Enova.Kai.IntegrationTests + deler av Infrastructure.Tests<br/>ekte Postgres/Blob via Testcontainers"]
UNIT["Unit — Domain / Application / Infrastructure / Worker / Architecture<br/>ren logikk, in-process, millisekunder"]
E2E --> INT --> UNIT
| Nivå | Prosjekt(er) | Hva det dekker | Avhengigheter |
|---|---|---|---|
| Unit | Enova.Kai.Domain.Tests, Enova.Kai.Application.Tests, Enova.Kai.Worker.Tests, Enova.Kai.Architecture.Tests, DeployContentUnderstanding.Tests |
Domenelogikk, use-cases, policy-objekter, job-orkestrering (med substitutter), arkitekturregler | Ingen — ren in-process, kjører på millisekunder |
| Unit + Integrasjon (blandet) | Enova.Kai.Infrastructure.Tests |
Ren regel-/judgment-logikk (raskt) og persistens-adaptere mot ekte Postgres | Testcontainers (Postgres) for persistens-testene |
| Integrasjon | Enova.Kai.IntegrationTests |
Adaptere og handlere mot ekte Postgres/Blob | Testcontainers (container-runtime kreves) |
| E2E | Enova.Kai.E2ETests |
Hele worker-pipelinen (SakSeleksjon → FetchDocuments → ExtractDocument → RunCaseWorkflow) + API-lesning, mot en delt Aspire-host med WireMock-stubbet Websak/Mimir/CU/Foundry | Aspire + Testcontainers + WireMock |
Testrammeverk: TUnit på Microsoft.Testing.Platform (MTP). Se CLAUDE.md for MTP-fellene
(--solution/--project er påkrevd; --filter er no-op, bruk --treenode-filter).
Prinsipp: test observerbar oppførsel, ikke implementasjon⚓︎
Tester skal verifisere offentlige kontrakter og observerbar oppførsel — API-svar, verdicts, persistert tilstand, kø-effekter — ikke private felter eller interne kall-sekvenser som ikke er en del av kontrakten. En test som knekker når du refaktorerer uten å endre oppførsel, tester implementasjonsdetaljer og bør skrives om. Konkret:
- Assertér på returverdier og tilstand (DB-rader, køede meldinger, HTTP-status/JSON), ikke på at en bestemt privat metode ble kalt.
- Bygg testdata gjennom de samme portene/fabrikkene som produksjonskoden bruker.
- Foretrekk data-drevne tester (
[Arguments]) for ren logikk med mange input/utfall — de er raske og dekker grensetilfeller billig (se f.eks. de deterministiske judgment-output-regleneElektriskTilbud/StottemottakerSamsvar, der C# regnerIsMet/Reasonut fra LLM-klassifiseringer).
Unit-først, ikke unit-only. Tyngdepunktet er raske unit-tester på den offentlige flaten til dype
moduler — bytt ut implementasjonen internt, og testen skal fortsatt passere. Men tre sømmer kan unit-
tester ikke dekke, og de får et tynt, raskt lag hver: EF-mapping mot ekte Postgres (InMemory takler
ikke Saks complex-types, AB#20964) → integrasjon; DI-/host-sammenstilling → E2E; worker-pipeline-
hopp (TickerQ, kø, worker→api) → E2E.
«Oppførsel» betyr forskjellige ting per lag. For ren logikk er oppførsel returverdien →
unit-test på det offentlige API-et. For en adapter uten I/O-avhengighet (rene oversetter-funksjoner som
SakReplikaOversetter.Oversett, eller en adapter som tar en IWebsakHttpClient vi kan substituere) er
oppførsel fortsatt en ren transformasjon → rask unit-test med substitutt, ikke container. For et
repository er oppførselen selve DB-rundturen → integrasjonstest som asserterer observerbare rader mot
ekte Postgres. Ikke mock det du faktisk tester; velg laget der kontrakten er observerbar.
Kjøring i CI⚓︎
Både kai-api og kai-worker kjører hele Kai.slnx (inkludert E2E) i validate-steget. E2E kjøres
altså i begge pipelinene med vilje: en endring i API-prosjektet skal også verifiseres ende-til-ende,
ikke bare av worker-pipelinen. Flaket som gjorde dette dyrt er fikset (delt Postgres-container +
HTTP/1.1 mot WireMock + retry på transiente steg-feil), så doblingen er lav risiko.
Validate er det eneste steget PR-validering kjører (branch policy på main). package (Docker)
og deploy er main-only. På main kjører package parallelt med validate — Docker-bygget er
selvstendig (egen restore+build i Dockerfile) og deler ingenting med validate — og deploy venter på
begge. Det kutter vegg-tiden på main-CI uten å legge noe i PR-løkka.
Ytelse — én delt Postgres-container⚓︎
Integrasjonstestene deler én pgvector-container for hele assembly-en, ikke én per testklasse.
PostgresFixture starter containeren én gang (lat, bak en gate) og gir hver testklasse sin egen
database inne i den delte containeren (CREATE DATABASE er millisekunder mot ~2-3s
container-oppstart). Isolasjonen mellom klasser er uendret — hver klasse har fortsatt sin egen
database — så [ClassDataSource<PostgresFixture>(Shared = PerClass)] står som før på hver klasse.
Dette kuttet Enova.Kai.IntegrationTests fra ~83s til ~12s.
Konvensjon: nye persistens-tester skal gjenbruke PostgresFixture (som gir en isolert database),
ikke starte sin egen container. Persistens-testene kjører mot ekte Postgres med vilje —
InMemory-provideren klarer ikke Saks enriched complex-type-egenskaper (AB#20964) — så «flytt til
in-memory» er ikke et alternativ for dem; den delte containeren er farten.
Stabilitet — kjente flake-kilder og vern⚓︎
- RulesEngine kjører ikke trådsikkert på delt
WorkflowName. Microsoft RulesEngine holder intern kompilerings-state nøklet påWorkflowName; parallelle tester som laster fixtures med samme navn racer og gir sporadisk feil verdict. Vern:[NotInParallel]på testklassen (seRulesEngineEvaluatorTests). Testene er in-process, så serialisering koster ingenting målbart. - E2E og HTTP/2-reset mot WireMock. WireMock.Net (Kestrel) tilbyr HTTP/2 via ALPN over TLS;
SocketsHttpHandleroppgraderer og treffer en kjent reset-flake (The response ended prematurely). Én slik reset veltet hele det delte E2E-fixturet. Vern: worker-subprosessen tvinges til HTTP/1.1 i E2E (DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_HTTP2SUPPORT=false, seKaiDistributedAppFixture). - Pipeline-steg som ikke slipper claimet på transient feil. En HttpClient-timeout kaster
OperationCanceledExceptionuten at jobbenscter kansellert. Fanges den som «shutdown» og rethrow-es utenMarkStepFailed, står steget claimet in-flight og saken re-prøves aldri. Vern: fang kunwhen (ct.IsCancellationRequested)som shutdown; alt annet faller tilMarkStepFailed→ TickerQ-retry (mønsteret iExtractDocumentJob,FetchDocumentsJob,RunCaseWorkflowJob). - Lokalt: E2E gjenbruker persistente podman-volumer (
kai-postgres-pg18,kai-azurite). Fixturet rydder testsakens tilstand, men akkumulert lokal state kan gi falske røde. Kjør på ren state (podman volume rm kai-postgres-pg18 kai-azurite) ved mistanke — CI kjører alltid friskt.
Dekning⚓︎
Kodedekning publiseres i CI (PublishCodeCoverageResults, cobertura). dotnet test kjører med
--coverage-settings codecoverage.config, som ekskluderer generert kode (EF Core-migrasjoner +
ModelSnapshot under Persistence/Migrations/). Uten den ekskluderingen ble ~31k linjer generert kode
telt med og dro totalen kunstig opp til ~96 %; det ærlige kildekode-tallet er ~90 %. Testassemblies er
allerede ekskludert som default i Microsoft.Testing.Platform.
Det er ingen hard terskel som blokkerer merge — i stedet fylles hull opp opportunistisk med raske tester på riktig nivå (se prinsippet under): ren logikk får data-drevne unit-tester, adaptere testes med substitutt/oversetter-funksjoner, og persistens/endepunkter dekkes av det raske integrasjonslaget.