Claims Service — Testing Strategy
Status: populated Owner: TBD Last updated: 2026-04-18 Companion: SERVICE_OVERVIEW · Service Template · 02 DDD
Coverage Targets
| Layer | Target |
|---|---|
| Unit (domain + application) | 90% line coverage |
| Integration | 80% branch coverage |
| Contract (Pact + FHIR) | All consumer contracts passing |
| E2E | Critical billing workflows covered |
Unit Tests
Domain and application layer tests run in-process with no external dependencies.
Domain tests:
ClaimRecord.assemble()— valid inputs produce draft claimClaimRecord.validate()— missing required fields throws validation errorClaimRecord.submit()— transitions draft→submitted correctlyClaimRecord.submit()— throws if status is notreadyClaimRecord.applyRemittance()— transitions accepted→paid with correct amountsClaimRecord.applyRemittance()— partial payment sets status topartial_paidClaimRecord.deny()— transitions accepted→denied, creates DenialCaseClaimRecord.appeal()— transitions denied→appealed, sets appeal deadlineCoverage.activate()/Coverage.deactivate()— status transitions- Money calculations — amount arithmetic with currency consistency check
- State machine invariants — all invalid transitions raise errors
Application use-case tests:
AssembleClaimUseCase— happy path produces claim and outbox eventAssembleClaimUseCase— invalid coding raises CODING_INVALIDAssembleClaimUseCase— duplicate claim raises DUPLICATE_CLAIMSubmitClaimUseCase— happy path calls adapter, updates statusSubmitClaimUseCase— adapter failure raises SUBMISSION_FAILEDProcessRemittanceUseCase— happy path generates EOBs and eventsVerifyEligibilityUseCase— active coverage returns eligibility resultVerifyEligibilityUseCase— adapter timeout → error stored, event emitted
Mandatory Integration Tests
These tests run against a real Postgres (testcontainers) and real NATS.
| Test | Description |
|---|---|
tenant-isolation | Claim created by tenant A is invisible to tenant B — asserts RLS policy |
cross-tenant-coverage | Coverage belonging to tenant A cannot be referenced in tenant B claim |
outbox-relay | Claim submitted → outbox record written → relay publishes event to NATS |
version-conflict | Concurrent update with stale version returns 409 |
eligibility-cache-expiry | Expired eligibility result returns ELIGIBILITY_EXPIRED error |
era-idempotency | Processing the same ERA twice is a no-op (no duplicate allocations) |
module-not-licensed | Claim creation returns 403 when tenant lacks ehr.claims entitlement |
fhir-eob-generated | ERA processing creates FHIR ExplanationOfBenefit resource |
appeal-deadline | Appeal filed after deadline raises CLAIM_INVALID_STATE |
Contract Tests
Consumer-Driven (Pact):
billing-service → claims-service:POST /api/v1/claimswith charge IDspatient-portal-service → claims-service:GET /fhir/R4/ExplanationOfBenefit?patient={id}population-health-service → claims-service: consumesclaims.claim.denied.v1event
FHIR Golden Fixture Tests:
Claimresource conforms to FHIR R4 base profileCoverageresource conforms to FHIR R4 base profileExplanationOfBenefitresource passes FHIR R4 validationCoverageEligibilityResponseresource conforms to FHIR R4 base profile
E2E Scenarios (Billing Workflows)
| Scenario | Description |
|---|---|
| Full claim lifecycle | Assemble → scrub → submit → receive ERA → apply payment → EOB visible |
| Denial and appeal | Claim denied → appeal filed → appeal approved → claim paid |
| Coverage verification | Add coverage → verify eligibility → attach to encounter → submit claim |
| Secondary billing | Primary claim paid → secondary claim created with COB adjustment |
| ERA multi-claim | Single ERA containing allocations for 5 claims — all correctly applied |
Adapter / Clearinghouse Mocking
Integration and E2E tests use a stub EDI adapter (StubClaimSubmissionAdapter) that:
- Returns a synthetic
clearinghouseRefonsubmit() - Returns a synthetic 999 acknowledgement after 100ms
- Supports configurable failure scenarios via test fixture injection
SFTP-based adapters are tested with an embedded SFTP server (Apache SSHD).