Skip to main content

housekeeping-service — TESTING_STRATEGY

Layered tests: domain → application → contract → integration → end-to-end. Coverage gate ≥ 85% lines / ≥ 80% branches for src/domain/** and src/application/**. Mutation testing on the domain quarterly. CI fails on any drop.


1. Test pyramid

┌──────────────────────┐
│ e2e (saga) │ ~10 scenarios
├──────────────────────┤
│ integration (db, │ ~80 scenarios
│ pubsub, sync, ai) │
├──────────────────────┤
│ contract / api │ 100% endpoints
├──────────────────────┤
│ application unit │ hundreds
├──────────────────────┤
│ domain unit │ thousands (fast)
└──────────────────────┘

Tooling: Jest + ts-jest, supertest for HTTP, testcontainers for Postgres + Pub/Sub emulator, node:test for the renderer-side sync simulator.

2. Domain unit tests (tests/unit/domain/**)

Pure, no I/O. Fast (< 10 ms each).

  • One spec per aggregate file, mirroring the file structure.
  • One spec per state-machine transition (allowed and denied).
  • Property-based tests for TimeWindow, LinenCount, ID factories using fast-check.
  • Invariant suites:
    • housekeeping-task.invariants.spec.ts
    • room-status.transitions.spec.ts
    • cleaning-checklist.frozen.spec.ts
    • inspection.completeness.spec.ts
    • linen-inventory.nonnegative.spec.ts

Coverage target: ≥ 95% lines for src/domain/**.

3. Application unit tests (tests/unit/application/**)

Use cases with in-memory ports (fakes, not mocks). Each use case has:

  • A happy-path spec.
  • A spec per domain error mapped.
  • A concurrency-conflict spec where applicable.
  • An idempotency spec (replay returns same outcome).
  • An outbox-append spec (asserts subject + payload shape).

Example matrix:

Use caseSpecs
CreateTaskUseCasehappy, room blocked, room not found, duplicate for room (open task), idempotent replay, outbox append shape
AssignTaskUseCasehappy, reassign, staff off-duty, transition denied, idempotent, outbox
CompleteTaskUseCasehappy, mandatory item missing, linen overdraw, room state flip emitted, with-inspection-required path
RequireMaintenanceUseCasepause path, terminal path, both emit room.maintenance_required.v1

Coverage target: ≥ 85% lines for src/application/**.

4. Contract tests (tests/integration/api-contract.spec.ts)

  • Loads contracts/openapi.yaml and asserts every operation responds with the declared shape (using openapi-response-validator).
  • Asserts every error response uses the RFC 7807 envelope and a valid MELMASTOON.HOUSEKEEPING.* code.
  • Asserts Idempotency-Key enforcement on mutating endpoints.
  • Asserts ETag emitted on aggregate reads and honoured on If-Match.

5. Integration tests (tests/integration/**)

Spin up Postgres + Pub/Sub emulator + a fake iam-service JWT issuer with testcontainers. Each suite is fully isolated (own database). Targets:

SuitePurpose
tenant-isolation.spec.tsAsserts RLS blocks cross-tenant reads/writes/updates on every table. MANDATORY.
db-schema.spec.tsAsserts live schema matches migrations/*.sql; rejects drift.
outbox-relay.spec.tsAsserts events are published once, in id order, with retry on transient publisher failures.
inbox-idempotency.spec.tsReplays the same (topic, message_id) 5× and asserts a single side-effect.
turnover-saga.spec.tsEnd-to-end: publish reservation.checked_out.v1 → assert task created, room flipped, outbox event emitted within 2 s p95.
early-checkout.spec.tsPriority bump path.
mid-stay-modification.spec.tsreservation.modification.requested.v1 with mid_stay_clean → task created.
maintenance-roundtrip.spec.tsrequireMaintenance → emits → consume maintenance.work_order.completed.v1 → new post_maintenance task.
room-status-state-machine.spec.tsProperty-based: random walks over allowed transitions; denied transitions return ROOM_STATE_CONFLICT.
inspection-flow.spec.tsPass + fail paths (re-clean creation).
linen-low-stock.spec.tsCrossing low-watermark fires alert exactly once per debounce window.
staffing-gap.spec.tsActive shifts vs queued task minutes triggers gap detection with debounce.
lost-and-found.spec.tsRecorded → matched → returned; recorded → disposed; retention auto-dispose path.
concurrency.spec.tsTwo clients reassign same task simultaneously → one wins, one gets 409.
partition-pruning.spec.tsAsserts hot reads use partition-pruned plans (EXPLAIN check).
oidc-pubsub-push.spec.tsRejects unsigned/expired/wrong-audience push tokens.

6. Sync tests (tests/integration/sync/**)

  • pull-cursor.spec.ts — first pull returns full set; subsequent pulls return only deltas.
  • push-conflict-policies.spec.ts — covers each policy (server_authoritative, lww+diff, append_only, max-of, client_wins_if_newer) with adversarial inputs; asserts audit_events row written on each conflict.
  • cursor-expiration.spec.ts — old cursor returns 410 SYNC_CURSOR_EXPIRED.
  • linen-derivation.spec.ts — pushed linen_movements recompute on_hand correctly.
  • op-id-replay.spec.ts — duplicate op_id returns the original outcome.

7. AI port tests

  • routing.adapter.spec.ts — request shape, timeout, fallback to empty suggestion.
  • routing-handler.spec.ts — applies suggestion only when no manual edits since generatedAt; otherwise surfaces banner only.
  • Circuit-breaker spec — opens after 5 consecutive failures, half-opens after 30 s.

8. Performance tests (perf/**, run weekly in staging)

k6 scripts:

  • board-read.k6.js — 200 active rooms, 50 RPS sustained, asserts p99 < 250 ms.
  • task-mutation.k6.js — 30 RPS mixed assign/start/complete, asserts p99 < 400 ms.
  • turnover-saga.k6.js — 10 events/s, asserts p95 ≤ 2 s.
  • sync-push.k6.js — bursts of 100 ops, asserts p99 < 1 s.

Failures cut a Jira perf-regression ticket and block the next prod release until acknowledged.

9. Resilience tests

  • Toxiproxy in front of Postgres / Pub/Sub: latency injection, packet loss, full disconnect — assert proper 503 / retry behaviour and no data loss.
  • Outbox relay killed mid-batch — restart processes resume from highest published_at.
  • Cloud Run instance kill mid-handler — duplicate inbox row prevents double effect.

10. Mutation testing

Stryker on src/domain/** quarterly. Threshold: ≥ 75% mutation score. Tracking ticket cut for any survived mutants.

11. Static analysis

  • TypeScript strict mode, noUncheckedIndexedAccess.
  • ESLint with @typescript-eslint, eslint-plugin-import, eslint-plugin-security.
  • tsc --noEmit in CI.
  • Prettier for formatting.
  • madge --circular to forbid circular module deps.
  • npm audit --production + Renovate for dep upgrades.
  • gitleaks in pre-commit.

12. CI gates (block merge)

GateThreshold
Lint0 errors
Type check0 errors
Domain unit tests100% pass, ≥ 95% lines, ≥ 90% branches
Application unit tests100% pass, ≥ 85% lines, ≥ 80% branches
Contract tests100% pass
Integration core (tenant-isolation, outbox-relay, inbox-idempotency, turnover-saga, room-status-state-machine)100% pass
OpenAPI ↔ controllers diffempty
Migration up + down on a fresh DBsuccess
gitleaks0 leaks

Nightly extended job runs the rest of the integration suite + sync + perf smoke. Weekly run executes full perf + Stryker.

13. Local testing

npm test runs unit + contract. npm run test:int runs integration with testcontainers (requires Docker). npm run test:perf:smoke runs a 60-second k6 smoke against localhost.