Gå til innhold

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-reglene ElektriskTilbud / StottemottakerSamsvar, der C# regner IsMet/Reason ut 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 (se RulesEngineEvaluatorTests). 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; SocketsHttpHandler oppgraderer 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, se KaiDistributedAppFixture).
  • Pipeline-steg som ikke slipper claimet på transient feil. En HttpClient-timeout kaster OperationCanceledException uten at jobbens ct er kansellert. Fanges den som «shutdown» og rethrow-es uten MarkStepFailed, står steget claimet in-flight og saken re-prøves aldri. Vern: fang kun when (ct.IsCancellationRequested) som shutdown; alt annet faller til MarkStepFailed → TickerQ-retry (mønsteret i ExtractDocumentJob, 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.