TESTING_STRATEGY — billing-service
Test pyramid is enforced in CI: unit (domain) is the widest layer, application wraps domain, integration verifies infrastructure adapters and DB, contract verifies API + event payload schemas, E2E exercises top user journeys, chaos verifies failure-mode SLOs.
1. Layers
| Layer | Tooling | Scope | Coverage gate |
|---|---|---|---|
| Unit (domain) | vitest (no I/O) | aggregates, value objects, invariants | line ≥ 95%, branch ≥ 90% |
| Application | vitest + in-memory ports | use cases against fake repositories, fake clock, fake outbox | line ≥ 90% |
| Integration | vitest + Testcontainers (Postgres, Pub/Sub emulator, GCS fake) | repositories, outbox drainer, event handlers, tax engine, PDF render | line ≥ 80% |
| Contract — API | vitest + supertest + the spec from API_CONTRACTS | request/response shapes vs. OpenAPI | 100% endpoints with at least 1 happy + 1 sad test |
| Contract — events | vitest + JSON schema validation | published payloads vs. schemas in EVENT_SCHEMAS | 100% event types |
| E2E | playwright | desktop (Electron) journeys + cloud REST | top 10 journeys |
| Chaos | toxiproxy, k6 | network / DB partition, slow Pub/Sub, processor brownouts | SLO assertions |
| Performance | k6 | hot endpoints | latency targets in OBSERVABILITY §3 |
2. Domain unit tests (the heart)
For every aggregate the suite exercises:
- happy path,
- every domain error in the matrix,
- every state transition (legal + illegal),
- OCC version increment,
- event emission shape (events as data, asserted whole).
Example shape:
describe('Folio.recordRefund', () => {
it('rejects when refund > net captured', () => {
const folio = openFolioFixture();
folio.postCharge(chargeFixture({ grossMicro: 100_000_000n }));
folio.recordPayment(paymentFixture({ amountMicro: 50_000_000n }));
expect(() => folio.recordRefund(refundFixture({ amountMicro: 80_000_000n })))
.toThrowDomainError('MELMASTOON.BILLING.REFUND_EXCEEDS_BALANCE');
});
});
Property-based tests (fast-check) cover:
Moneyarithmetic (associativity, identity, currency-mismatch surfacing),FXSnapshot.convertround-trip within ε,Folio.balanceInFolioCurrency()algebraic identityΣchg + Σtax − Σpay + Σreffor any random sequence of mutations.
3. Application use-case tests
Use in-memory FolioRepository, OutboxWriter, Clock, IdGenerator doubles. Patterns covered:
- idempotency: same
Idempotency-Key⇒ identical output, no double-emit, single side-effect; - inbox dedupe for events;
- OCC retry path (3 attempts, then surfaced);
- error → HTTP mapping table assertions for the controller layer above;
- compensating actions (e.g., refund → gateway failure → folio refund rolled back, no orphan credit note).
4. Integration tests
Single Testcontainers stack per CI job:
- Postgres 16 (per-tenant schemas created by the templated DDL bundle);
gcr.io/google.com/cloudsdktool/google-cloud-cliPub/Sub emulator;fsouza/fake-gcs-serverfor invoice PDFs.
Suites:
- TenantProvisioning — create three tenants; verify schemas, RLS policies, GUC default, triggers.
- FolioRepository — open / save / load with
versionOCC; cross-tenant access blocked. - OutboxDrainer — write event in tx, drainer publishes to emulator topic, sets
published_at; failure path leaves row. - InvoicePipeline — close folio of fixture types: standard EN, government AR (RTL, bilingual numerals), corporate USD, agent EUR, sharia PKR (no interest line). Snapshot the rendered PDF metadata and assert
sha256(payload)matches the embedded hash. - CashDrawerLifecycle — open, post receipts, initiate close, finalize close (mock
iam-servicestep-up), variance scenarios (within / over threshold), prior-session-open guard. - SubscriptionCycle — provision tenant, ingest usage events, run cycle worker, assert invoice rows + outbox events; failure → dunning state advance.
- TaxEngine — fixture-driven matrix per jurisdiction (AF-KBL, AF-HER, PK-KHI, SA-RUH, AE-DXB, TJ-DYU, IR-THR) × charge kind × customer class; 100% match required.
- MultiCurrencyFolio — folio AFN with USD payment; assert per-currency totals and FX rounding to half-up banker's at minor unit.
- Reconcile — daily reconciliation finds mismatch between cash sessions and folio cash payments; emits the right event.
5. Contract tests
- OpenAPI —
redocly lint+openapi-diffagainst the previous main; breaking changes blocked unless the PR is labeledapi-breaking-v2. - Event schemas —
ajvvalidates fixtures from each emitter against the JSON schemas in EVENT_SCHEMAS. - Consumer-driven —
pact-brokerinteractions withbff-backoffice-service,bff-tenant-booking-service,notification-service,analytics-service; PR fails if a deployed consumer's contract no longer matches.
6. E2E (Playwright)
Top 10 journeys:
- Walk-in: open folio → post charges → record cash payment → close → print invoice.
- Booking → check-in → mini-bar charges → external card payment → checkout → invoice (multi-currency).
- Reservation cancellation with policy-based partial refund + credit note.
- Cash drawer open → posted shifts of receipts → initiate close → online finalize close (two staff via mock step-up).
- Cash drawer close with variance > threshold → reconciliation_blocked → next session blocked → supervisor acknowledges → next session opens.
- Folio re-open after checkout → add restaurant charge → close → new invoice + voided original.
- Subscription billing cycle: usage ingested over month → cycle runs → invoice generated → payment captured → state stays
current. - Subscription payment failed → grace → past_due → suspended → reactivation by platform admin.
- RTL Arabic invoice render with bilingual numerals (visual regression baseline).
- Sharia-compliant tenant attempts to add
interestlate fee → rejected; correct invoice template applied with no finance-charge wording.
E2E runs in a containerized Electron with a virtual display; recorded videos and screenshots upload to the CI artifact store on failure.
7. Chaos & resilience
| Scenario | Tool | Assertion |
|---|---|---|
| Cloud SQL primary failover | gcloud sql instances failover in stage | folio mutations resume in ≤ 30 s; no double-write |
| Pub/Sub publish 5 s latency | toxiproxy | outbox lag rises but drainer recovers; no event loss |
payment-gateway-service 5xx | mock proxy 503 | refund use case rolls back domain state; no orphan credit note |
iam-service step-up rejected | mock returns 401 | cash session close fails clean; session stays pending_close |
| Network partition during cash close | toxiproxy cut mid-request | desktop surfaces "queued"; on reconnect, replay finalizes idempotently |
| Tax-engine cache cold-start | tenantSettings proxy 500ms | charge post latency budget held by retry+circuit |
| AI orchestrator timeout | toxiproxy 100% timeouts | fallback triggers; no impact on user request latency |
8. Performance / load
k6 scenarios run nightly against staging:
- 200 rps
POST /folios/:id/chargesper tenant for 10 minutes → p95 ≤ 350 ms; - 50 rps
POST /folios/:id/closeper tenant for 5 minutes → p95 ≤ 2 s including PDF render; - 100 concurrent active cash sessions per tenant, mixed open/post/close.
Regressions ≥ 10% over the rolling baseline block the PR.
9. Security tests
- PCI-DSS DLP scan — pre-commit and CI rule rejects strings matching Luhn-passing 13–19 digit sequences in repo, in
_outboxpayloads (sample), and in logs (sample). - AuthZ matrix tests — every endpoint × every role; 403 expected for un-scoped roles; step-up enforced where required.
- OCC tampering — replay attack with stale
If-Match; expect 412. - Cross-tenant — request signed with tenant A token attempting to read tenant B
folioId; expect 404 (not 403, to avoid leaking existence).
10. Test data & fixtures
- Fixture builders in
test/fixtures/produceFolio,Charge,Payment,Refund,Subscriptiondeterministically from a seed. - Currency / tax / locale matrix fixture lives at
test/fixtures/tax-matrix.jsonand is the source of truth for theTaxEngineintegration suite. - PDF golden snapshots live at
test/fixtures/invoice-snapshots/keyed by(template, locale)— diffs reviewed visually in PR.
11. Coverage & CI gating
Coverage thresholds enforced by vitest --coverage:
- domain: line 95% / branch 90% / function 100%
- application: line 90% / branch 85%
- combined: line 85% / branch 80%
npm run check runs: lint → typecheck → unit → application → integration → contract. PRs fail if any threshold or contract / OpenAPI gate fails. E2E + chaos + performance run on the merge queue and nightly.
12. Cross-references
- Domain invariants under test: DOMAIN_MODEL §8.
- Use cases under test: APPLICATION_LOGIC.
- API and event contracts: API_CONTRACTS, EVENT_SCHEMAS.
- Failure scenarios under chaos: FAILURE_MODES.
- SLOs that load tests defend: OBSERVABILITY §3.