Patient Chart Service — Testing Strategy
Status: populated Owner: TBD Last updated: 2026-04-18 Companion: Service Template · TESTING_STANDARDS
1. Coverage targets
| Layer | Target | Tool |
|---|---|---|
| Domain (aggregates, invariants, value objects) | ≥ 90 % | Vitest |
| Application (use cases, ports) | ≥ 85 % | Vitest |
| Infrastructure (adapters) | ≥ 70 % | Vitest + TestContainers |
| Integration (full stack) | ≥ 70 % | Vitest + TestContainers |
| Contract | 100 % of published events + consumed events | Pact + schema registry |
| E2E (critical flows) | Mandatory flows only | Playwright / Supertest |
2. Test types and mandatory tests
2.1 Unit tests (test/unit/)
| Aggregate / module | Key scenarios |
|---|---|
Problem | All state transitions; entered-in-error with/without reason; duplicate problem detection |
Allergy | NKA rule conflict; NKDA conflict; duplicate substance; reaction severity combinations |
VitalsSet | BMI derivation; range validation (warn/reject policy); LOINC code requirement; immutable correction model |
ClinicalNote | Draft→signed transition; cosign-required block; addendum post-sign; AI provenance missing → reject |
ChartAccess | Break-glass reason required; sensitive-segment denial |
| Domain errors | All error codes in DOMAIN_MODEL.md §10 tested |
2.2 Integration tests (test/integration/) — MANDATORY
| Test file | What it verifies | Block on failure |
|---|---|---|
tenant-isolation.spec.ts | ten_dev_a data is NEVER readable by ten_dev_b at REST and DB level; RLS enforced | Hard block |
outbox.spec.ts | Every domain event written to outbox in same transaction as aggregate; relay publishes to NATS | Hard block |
inbox.spec.ts | registration.patient.merged.v1 and gdpr.subject_request.received.v1 are idempotently handled | Hard block |
allergy-advisory.spec.ts | Advisory returns correct allergy list with severity; NKA returns advisory with isNKA=true; concurrent allergy write handled | |
vitals-range-validation.spec.ts | Hard-stop fires when reject policy set; warn policy allows record with flag | |
note-cosign-routing.spec.ts | SignNote blocks without cosign when policy requires; pending_cosign state set | |
breakglass.spec.ts | Break-glass without reason → 422; with reason → access granted + breakglass.invoked.v1 emitted | |
ai-provenance.spec.ts | AcceptAIChunk without provenanceId → CHART_AI_PROVENANCE_MISSING; with provenance → note.ai_accepted.v1 emitted | |
fhir-read.spec.ts | Condition, AllergyIntolerance, Observation FHIR read surface returns conformant resources |
2.3 Contract tests (test/contract/)
Provider contracts (patient-chart-service publishes):
| Event | Schema conformance test |
|---|---|
patient_chart.problem.added.v1 | problem-added.schema.spec.ts |
patient_chart.allergy.added.v1 | allergy-added.schema.spec.ts |
patient_chart.vitals.recorded.v1 | vitals-recorded.schema.spec.ts |
patient_chart.note.signed.v1 | note-signed.schema.spec.ts |
patient_chart.note.ai_accepted.v1 | note-ai-accepted.schema.spec.ts |
patient_chart.breakglass.invoked.v1 | breakglass-invoked.schema.spec.ts |
Consumer contracts (patient-chart-service is consumer):
| API | Pact consumer test |
|---|---|
GET /v1/ai/assist (ai-gateway-service) | ai-gateway-provider.pact.spec.ts |
GET /v1/terminology/lookup (terminology-service) | terminology-provider.pact.spec.ts |
GET /v1/patients/:id (registration-service) | registration-provider.pact.spec.ts |
2.4 E2E tests (test/e2e/)
| Flow | Test |
|---|---|
| Add problem → allergy → vitals → create note draft → sign note | chart-core-flow.e2e.spec.ts |
| AI-assist → HITL accept → note signed with provenance | chart-ai-assist-flow.e2e.spec.ts |
| Break-glass → chart access → audit entry in audit-service | breakglass-audit-flow.e2e.spec.ts |
| GDPR erasure saga → author PII redacted, clinical content retained | gdpr-erasure.e2e.spec.ts |
| Allergy advisory sync → medication-service receives advisory | allergy-advisory-consumer.e2e.spec.ts |
3. Test data strategy
- All test data uses synthetic patients with
pat_dev_*prefix; no real PHI. __builders__directory in each aggregate domain folder provides builder functions:ProblemBuilder.active(),AllergyBuilder.nka(), etc.- TestContainers provides a real Postgres 16 instance per integration test run (isolated schema per test file).
- NATS JetStream mock via
@nestjs/testingfor unit; real NATS via TestContainers for integration.
4. CI pipeline
┌─────────────────────────────────────────────┐
│ 1. lint + typecheck │ ← fail fast
│ 2. unit tests (vitest) │ ← no external deps
│ 3. integration tests (TestContainers) │ ← real Postgres + NATS
│ ├── tenant-isolation.spec.ts (MANDATORY) │
│ ├── outbox.spec.ts (MANDATORY) │
│ └── inbox.spec.ts (MANDATORY) │
│ 4. contract tests (Pact broker) │
│ 5. schema conformance tests │
│ 6. coverage gate (≥ 80 % overall) │
│ 7. e2e tests (staging deploy) │
└─────────────────────────────────────────────┘
5. Quality gates
| Gate | Threshold | Enforced by |
|---|---|---|
| Overall coverage | ≥ 80 % | Vitest coverage report in CI |
| Domain coverage | ≥ 90 % | Vitest --coverage with domain path filter |
tenant-isolation | Must pass | CI hard block |
| Pact verification | Must pass | Pact broker CI step |
| Schema conformance | Must pass | Schema registry CI step |
| Build time (unit) | < 60 s | CI timeout |
| Build time (integration) | < 300 s | CI timeout |
6. Test naming conventions
// Pattern: describe aggregate + scenario + expectation
describe('Allergy.record', () => {
it('rejects substance allergy when NKA is active', () => { ... })
it('allows medication allergy when only NKDA is active and category is food', () => { ... })
})
describe('ClinicalNote.sign', () => {
it('blocks signing when cosign policy requires and cosigner is not attested', () => { ... })
it('transitions to signed state when no cosign policy required', () => { ... })
})