Skip to main content

Testing Strategy and QA — Ghasi-eHealth Platform

Status: populated Last updated: 2026-04-18

Cross-reference: Testing Standards · Definition of Done


1. Purpose and Scope

This document defines the platform-wide testing strategy for the Ghasi-eHealth national eHealth platform. All 27 services inherit these defaults. Service-specific deviations are documented in each service's TESTING_STRATEGY.md. This strategy covers:

  • Unit, integration, contract, E2E, visual, performance, chaos, and security test types.
  • Coverage targets per architectural layer.
  • Mandatory per-service integration test files.
  • Consumer-driven contract testing via Pact.
  • Event schema conformance testing.
  • FHIR profile validation.
  • Load testing patterns with k6.
  • Accessibility testing.
  • Test data management.
  • CI pipeline gate definitions.

2. Layer-Based Coverage Targets

All Ghasi-eHealth services follow the hexagonal architecture (domain/, application/, infrastructure/, presentation/). Coverage targets are layer-stratified:

LayerMinimum coverageRationale
domain/ (aggregates, value objects, domain services)≥ 95% line; 100% VOBusiness invariants and safety rules must be exhaustively tested
application/ (use cases, orchestration, ports)≥ 85% lineOrchestration paths have fewer combinatorial branches but all paths must pass
infrastructure/ (adapters, DB, NATS, HTTP clients)≥ 75% lineIntegration tests cover most paths; unit tests cover adapter mapping logic
presentation/ (controllers, guards, DTOs)≥ 70% lineContract tests plus controller unit tests cover the key paths
Overall service≥ 80% line; ≥ 75% branchEnforced in CI via coverageThreshold in vitest.config.ts

Safety-critical services (orders-service, medication-service, laboratory-service, identity-service, registration-service): domain layer target is ≥ 95%; application layer ≥ 90%.

// vitest.config.ts — standard service threshold
coverageThreshold: {
global: { branches: 75, functions: 80, lines: 80, statements: 80 },
'./src/domain/**': { branches: 90, functions: 95, lines: 95, statements: 95 },
'./src/application/**': { branches: 80, functions: 85, lines: 85, statements: 85 },
},

3. Test Types

3.1 Unit Tests

Framework: Vitest (Jest-compatible API) Scope: domain layer (aggregates, value objects, domain events, invariants) + application layer (use case logic with mocked ports)

Mandatory per service:

  • Every aggregate state machine transition.
  • Every invariant violation (assert throws correct domain error).
  • Every value object construction (valid + invalid).
  • Every use case happy path and each documented error path.
  • Every guard (RBAC, module entitlement).

Rules:

  • Mock all ports (repositories, NATS, HTTP clients, CDS). Test the SUT, not its dependencies.
  • Reference Business Rule IDs in test descriptions: it('BR-ORD-003: cancelling a completed order throws ORDER_TERMINAL_STATE').
  • No real DB, real NATS, or real HTTP in unit tests.

3.2 Integration Tests

Framework: Vitest + Testcontainers (PostgreSQL 16, NATS 2.10)

Scope: Infrastructure layer + cross-layer workflows through real DB and NATS.

Mandatory per-service files (all three must be green before merge):

FileWhat it tests
test/integration/tenant-isolation.spec.tsAll repository read/write operations are tenant-scoped. Data created for tenant_A is invisible to tenant_B under RLS. Insert cross-tenant IDs via raw SQL, confirm RLS blocks them from service queries.
test/integration/outbox.spec.tsDomain event is written to outbox table in same transaction as aggregate write. Relay worker picks up and publishes to NATS. Event is idempotent on re-delivery (inbox dedupe on consumer side).
test/integration/inbox.spec.tsInbound NATS event is de-duplicated (message_id idempotency key). Processing is applied once even if event is delivered twice.

Additional integration test areas:

  • DB migrations apply cleanly on a fresh schema.
  • Repository CRUD — happy path and not-found paths.
  • FHIR adapter mapping round-trip (internal entity → FHIR resource → back).
  • Optimistic lock conflict on concurrent writes.
  • Idempotency-Key header returns cached response for 24 h.

Test isolation: Each test suite runs in a DB transaction rolled back after the suite. Testcontainers creates a fresh instance per CI run.

// test/integration/tenant-isolation.spec.ts shape
describe('tenant isolation — orders', () => {
it('orders created for tenantA are not returned for tenantB', async () => {
// Arrange: insert order for tenantA via raw SQL (bypass RLS)
// Act: query via repository with tenantB session variable
// Assert: result is empty
});
it('RLS blocks direct SELECT without session variable', async () => {
// Assert: query without app.tenant_id returns 0 rows
});
});

3.3 Consumer-Driven Contract Tests (Pact)

Framework: Pact (pactflow.io broker or self-hosted Pact Broker) Pattern: Consumer writes the contract; provider verifies it on every PR.

Workflow:

Contract test locations:

test/contract/
├── <event>.schema.spec.ts ← event schema conformance (see 3.4)
└── <endpoint>.pact.spec.ts ← REST consumer-driven contracts

Per-service requirements:

  • Every public REST endpoint with an external consumer must have a Pact contract.
  • Producer publishes new versions to Pact broker on merge to main.
  • can-i-deploy gate in CD pipeline before staging deploy.
  • Consumer tests must be green against provider before any API breaking change is merged.

REST contract test template:

// test/contract/orders-activate.pact.spec.ts (consumer side)
import { PactV3, MatchersV3 } from '@pact-foundation/pact';

const provider = new PactV3({ consumer: 'laboratory-service', provider: 'orders-service', ... });

describe('orders-service — order activated event', () => {
it('publishes order.activated with required fields', async () => {
await provider.addInteraction({ /* ... */ }).executeTest(async (mockProvider) => {
// Assert consumer can parse the published event
});
});
});

3.4 Event Schema Conformance Tests

Framework: AJV (JSON Schema validation) against schemas registered in the schema registry.

Pattern: Every event producer registers a JSON schema at event-schemas/{service}/{aggregate}/{event}/v{N}.json. Conformance tests validate that produced events match the registered schema.

// test/contract/order-activated.schema.spec.ts
import schema from '../../event-schemas/orders/order/activated/v1.json';
import Ajv from 'ajv';

it('order.activated.v1 payload matches registered schema', () => {
const ajv = new Ajv({ strict: true });
const validate = ajv.compile(schema);
const event = buildOrderActivatedEvent(); // factory
expect(validate(event)).toBe(true);
});

CI gate: Event schema conformance tests run on every PR that modifies an event producer. Schema registry diff gate prevents field removal without version bump.

3.5 FHIR Profile Validation Tests

Framework: HAPI FHIR Validator (Java CLI via Docker) or fhir-validator-js wrapper.

What is tested:

  • Every FHIR resource type produced by the service validates against the declared FHIR profile (US Core 6.1, IPS, AFG-Core where applicable).
  • CapabilityStatement declares all owned resource types.
  • OperationOutcome format for all FHIR error responses.
// test/contract/serviceRequest.fhir.schema.spec.ts
import { validateFhirResource } from '@ghasi/fhir-test-utils';

it('maps Order to conformant US Core ServiceRequest', async () => {
const serviceRequest = mapOrderToServiceRequest(buildOrder({ orderType: 'laboratory' }));
const result = await validateFhirResource(serviceRequest, 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-servicerequest');
expect(result.issues.filter(i => i.severity === 'error')).toHaveLength(0);
});

CI gate: FHIR conformance tests block merge if any FHIR resource produces errors (not just warnings) against the declared profile.

3.6 E2E Tests (Playwright)

Framework: Playwright 1.x Scope: Critical user flows from browser through Kong to real services deployed in the test environment.

Critical paths covered:

PathServices involved
Login → patient chart → create medication order → view CDS alertidentity, registration, orders, cds-engine
Login → create lab order → verify lab worklist entryidentity, registration, orders, laboratory
Login → create referral → verify scheduling-service receives referral eventidentity, registration, orders, scheduling
Patient portal: login → view active orders and care plansidentity, patient-portal, orders, care-plan
Admin: manage order sets → clinician instantiates order setidentity, orders
Claims: register coverage → submit claimidentity, claims

Test conventions:

  • No page.waitForTimeout — use page.waitForSelector or expect(locator).toBeVisible().
  • Each test creates isolated tenant data via API setup hook.
  • Visual regression screenshots per test at 1440 px and 375 px (mobile).

Accessibility gate: Every E2E test run includes an axe-core scan on each page visited. Any serious or critical axe violation blocks merge.

3.7 Visual Regression Tests

Framework: Playwright screenshot comparison + Percy / Chromatic (CI)

Scope: Key clinical UI surfaces — order entry form, CDS alert banner, referral detail, order set selector.

Breakpoints tested: 375 px (mobile), 768 px (tablet), 1440 px (desktop).

Policy: Unreviewed visual diffs block the PR. Reviewer must approve or reject visual change before merge.

3.8 Performance / Load Tests (k6)

Framework: k6 (Grafana k6 Cloud or self-hosted)

Scenarios per service:

ScenarioWhat it testsPass criteria
Baseline — 10 concurrent users for 5 minNormal operating loadp95 < SLO target; error rate < 0.5%
Soak — 30 concurrent users for 60 minMemory leaks, connection pool exhaustionp95 stable over duration; no OOM
Stress — ramp 5→100 concurrent over 10 minBreaking point discoveryIdentify cliff; note max sustainable load
Spike — 0→500 concurrent in 30 sElastic scaling behaviourHPA scales; error rate recovers within 60 s

Safety-critical service baselines (orders-service, medication-service):

// k6 baseline — order creation with CDS
export const options = {
scenarios: {
order_create_baseline: {
executor: 'constant-vus',
vus: 50,
duration: '5m',
},
},
thresholds: {
'http_req_duration{name:create_order}': ['p(95)<800'],
'http_req_failed': ['rate<0.005'],
},
};

export default function () {
const payload = JSON.stringify(buildOrderPayload());
const res = http.post(`${BASE_URL}/v1/orders`, payload, { headers });
check(res, { 'status is 201': (r) => r.status === 201 });
}

Platform-wide performance targets:

Endpoint typep95 targetp99 target
Simple reads (single resource)< 200 ms< 500 ms
Complex reads (list with filters)< 600 ms< 1 200 ms
Writes (with validation)< 500 ms< 1 000 ms
Writes (with CDS / external call)< 800 ms< 2 000 ms
FHIR bulk export< 30 s for 1 000 resources

k6 tests run in CI: Baseline scenario runs on every PR for safety-critical services. Full soak and stress run weekly on the staging environment.

3.9 Chaos Engineering

Framework: Chaos Mesh (Kubernetes-native) / Litmus

Chaos scenarios run quarterly in staging:

ScenarioTargetExpected behaviour
Kill 1 of 3 orders-service podsorders-serviceHPA replaces pod; p95 stays < 800 ms
Inject 500 ms NATS latencyNATS JetStreamOutbox relay delays; orders still created; events delivered within 60 s
Kill PostgreSQL primaryPrimary DB nodeStandby promotes; service recovers within 60 s
Inject DNS resolution failure for CDS engineCDS endpointorders-service enters CDS degraded mode; medication activation blocked; ops alert fires
Network partition between regionsCross-region linksTenants in unaffected region unimpacted; affected tenants gracefully degraded

Chaos runbook: infra/runbooks/chaos-playbook.md

3.10 Security Tests (OWASP ZAP)

Framework: OWASP ZAP (automated baseline scan + authenticated active scan)

Run schedule: Weekly against staging environment + before every major release.

Scan profiles:

Scan typeFrequencyPass criteria
ZAP baseline (passive)Every PR to main0 High, 0 Critical findings
ZAP authenticated active scanWeekly (staging)0 High, 0 Critical new findings
Dependency audit (pnpm audit)Every PR0 Critical severity packages
security-reviewer agentEvery PR touching auth/payment/data0 Critical findings

Mandatory test coverage for security paths:

  • 401 on every endpoint without token.
  • 403 on every endpoint with insufficient role.
  • 403 MODULE_NOT_ENTITLED when tenant lacks module license.
  • RLS cross-tenant queries return empty (not 403).
  • No PII in response for unauthorized caller.

4. Mandatory Per-Service Integration Tests

Every service in the platform must have these three files in test/integration/ — all must be green before merge:

test/integration/
├── tenant-isolation.spec.ts ← MANDATORY — blocks merge if failing
├── outbox.spec.ts ← MANDATORY — blocks merge if failing
└── inbox.spec.ts ← MANDATORY — blocks merge if failing

4.1 tenant-isolation.spec.ts Requirements

// Minimum test cases (extend with service-specific reads)
describe('tenant isolation', () => {
it('data written for tenantA is not visible to tenantB');
it('RLS is enforced — no session variable returns 0 rows');
it('cross-tenant FK references are rejected by domain aggregate');
it('list queries with no tenantId filter return empty, not all-tenant data');
});

4.2 outbox.spec.ts Requirements

describe('outbox', () => {
it('domain event is written to outbox in same transaction as aggregate write');
it('relay publishes event to NATS within 5 seconds of commit');
it('relay does not publish duplicate event if outbox row already delivered');
it('relay retries if NATS is temporarily unavailable');
it('relay marks event delivered after ACK from NATS');
});

4.3 inbox.spec.ts Requirements

describe('inbox', () => {
it('processing the same event twice has no duplicate side effect');
it('processed event is recorded in inbox_processed table');
it('event with unknown type is logged and discarded, not thrown');
it('inbox processing is transactional with the aggregate update');
});

5. Test Data Management

5.1 Factories (not raw fixtures)

All test data is produced via factory functions. No hardcoded IDs or raw SQL fixtures outside migration scripts.

// Example factory — domain/__builders__/order.builder.ts
export function buildOrder(overrides: Partial<Order> = {}): Order {
return {
id: `ord_${ulid()}`,
tenantId: overrides.tenantId ?? 'ten_01TEST',
patientId: overrides.patientId ?? `pat_${ulid()}`,
encounterId: overrides.encounterId ?? `enc_${ulid()}`,
orderType: 'laboratory',
status: 'draft',
priority: 'routine',
orderedBy: `usr_${ulid()}`,
orderedAt: new Date().toISOString(),
cdsAlerts: [],
version: 1,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
};
}

5.2 Anonymised Prod-Like Datasets

Integration and performance tests use anonymised prod-like datasets (not real patient data):

DatasetSizeUse
seed-small100 patients, 500 encounters, 1 000 ordersDefault integration tests
seed-medium10 000 patients, 50 000 encountersBaseline performance tests
seed-large100 000 patientsSoak and stress tests

Seed datasets generated by scripts/generate-anonymised-seed.ts using Faker.js with medically plausible codes from terminology-service fixtures. No real patient identifiers.

5.3 Seed Scripts

pnpm db:seed # seed-small (default for dev)
pnpm db:seed:medium # seed-medium (for perf tests)
pnpm db:seed:reset # truncate all tables and re-seed

5.4 Isolation Policy

EnvironmentIsolation mechanism
Unit testsIn-memory mocks; no DB
Integration testsTestcontainers — fresh container per CI run; DB transaction rollback per test file
E2E testsIsolated tenant per test suite; cleanup hook after suite
Performance testsDedicated performance tenant; seeded data not shared with functional tests

6. CI Pipeline Gates

6.1 Merge Gate (PR → main)

All of the following must be green before a PR is merged:

GateToolFailure action
Lint + formatESLint + PrettierBlock merge
TypecheckTypeScript strictBlock merge
Unit testsVitestBlock merge
Coverage thresholdsVitest coverageBlock merge
tenant-isolation.spec.tsVitest + TestcontainersBlock merge
outbox.spec.tsVitest + TestcontainersBlock merge
inbox.spec.tsVitest + TestcontainersBlock merge
Event schema conformanceAJVBlock merge
Pact consumer testsPactBlock merge (for consumer changes)
FHIR conformance (affected services)HAPI FHIR ValidatorBlock merge
Security: ZAP baselineOWASP ZAP passiveBlock if Critical/High
Security: pnpm auditpnpmBlock if Critical
OpenAPI diff gateopenapi-diffBlock on breaking change without version bump

6.2 Deploy Gate (main → staging)

GateToolFailure action
All merge gatesBlock deploy
Pact can-i-deployPact BrokerBlock deploy
Pact provider verificationPactBlock deploy
k6 baseline performance (safety-critical services)k6Block deploy if p95 > SLO
ZAP authenticated scanOWASP ZAPBlock if Critical/High new findings
Helm chart dry-runHelmBlock deploy on template errors

6.3 Deploy Gate (staging → production)

GateToolFailure action
All staging gatesBlock
Canary deploy (5% / 30 min)Kong weighted routingBlock full rollout if error rate > 1%
Rollback test (manual, quarterly)Manual + runbookDocument result
SLO status greenGrafana SLOBlock if SLO burn rate critical
SERVICE_READINESS.md signed offHuman reviewBlock

7. Accessibility Testing

Standard: WCAG 2.2 Level AA Framework: axe-core via @axe-core/playwright

Coverage scope:

  • Every clinician-facing UI page (EHR, orders, care plans, scheduling).
  • Every patient-facing portal page.
  • Every admin UI surface.

Integration with E2E tests:

import AxeBuilder from '@axe-core/playwright';

test('order entry form is accessible', async ({ page }) => {
await page.goto('/orders/new');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
.analyze();
expect(results.violations.filter(v => ['critical', 'serious'].includes(v.impact))).toHaveLength(0);
});

RTL support: All UI surfaces tested in both LTR (English) and RTL (Dari/Pashto) modes. Logical CSS properties (padding-inline, margin-block) enforced — no directional properties in component CSS.

Reduced motion: Animations gated on prefers-reduced-motion: reduce; Playwright tests run with this preference set.


8. Cross-Service Test Coordination

8.1 Pact Broker Configuration

AttributeValue
Broker URLhttps://pact.internal.ghasi-ehealth.af
AuthenticationBearer token (Vault-managed)
Webhook: on verification publishedNotify consumer CI of provider verification result
Webhook: can-i-deployCalled in CD pipeline before staging deploy

Tag strategy: Each service publishes pacts tagged with branch:{branch-name} and version:{semver}. can-i-deploy checks the main tag of the provider.

8.2 Schema Registry

Event schemas live at:

event-schemas/
└── {service}/
└── {aggregate}/
└── {event}/
└── v{N}.json

Schema registry CI gate: any PR that adds or modifies an event must include the corresponding schema file. Removing a required field without a version bump is blocked.


9. Testing Anti-Patterns (Platform-Wide)

Anti-patternWhy bannedCorrect approach
synchronize: true in Testcontainers DBSchema drift from prod migrationsAlways run migrations in test DB
Shared mutable state between testsFlaky tests; order-dependent failuresbeforeEach cleanup; factory per test
page.waitForTimeout(3000) in PlaywrightSlow and fragilewaitForSelector / expect.toBeVisible()
Testing only happy paths in domainMisses invariant enforcementEvery invariant has a negative unit test
Snapshot tests for API response shapesBrittle; intent hiddenAssert specific fields and types
Skipping tenant-isolation.spec.tsCross-tenant data leak riskTest is mandatory and blocking
Mocking the system under testTests prove nothingMock dependencies only; test real SUT
Hard-coded patient IDs in testsTests fail after seed changesUse builder factories; generate ULIDs

10. Reporting and Visibility

ReportToolAudienceFrequency
Coverage reportVitest HTML / CodecovEngineeringPer PR
Test resultsJUnit XML → CI dashboardEngineeringPer CI run
Pact verification matrixPact Broker UIEngineeringContinuous
k6 performance trendsGrafana k6 dashboardSRE + EngineeringPer weekly soak
Accessibility violationsaxe HTML report → CI artifactEngineering + UXPer E2E run
Security scan resultsZAP HTML report → CI artifactSecurity + EngineeringWeekly
FHIR conformance reportHAPI FHIR validator outputEngineering + interop teamPer PR (affected services)

11. Ownership and Escalation

IssueOwnerEscalation
Coverage threshold regressionService tech leadEngineering Manager
tenant-isolation.spec.ts failureService tech lead (P0 — same day)CISO + Engineering Manager
Pact verification brokenConsumer + provider tech leadsShared resolution within 24 h
k6 p95 regression > SLOSRE + tech leadSRE Director
ZAP Critical findingSecurity teamCISO — same day
Accessibility serious/critical violationFront-end leadAccessibility review within sprint