# Test Strategy

> Backend testing pyramid — unit for pure logic, integration against a real DB (Testcontainers), and consumer-driven contract testing (Pact) for service boundaries. Use before a feature, after a bug fix, or when services break each other on deploy. Not for load testing (use performance-profiling) or security testing (use backend-security-audit).

- Skill: `jaykim88/test-strategy` (Agent Skill)
- Install (CLI): `npx skillmds@latest add jaykim88/test-strategy`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jaykim88/test-strategy/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- License: MIT
- Author: JayKim88 (https://skillmd.com/u/jaykim88)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/jaykim88/test-strategy

---


# Test Strategy (Backend)

## Purpose
Test the backend where bugs actually live — integration against a real database, contract tests at service boundaries — not a wall of mocked unit tests that pass while production breaks.

**Universal** — the backend testing pyramid, real-DB integration testing, and consumer-driven contract testing are principles; the runner/container/Pact-binding differs by language.

## Procedure

1. **Unit tests — pure logic only**
   - Pure functions, domain rules, calculations — fast, no I/O
   - Don't unit-test glue code by mocking everything; that tests the mocks, not the system

2. **Integration tests — against a REAL database (the bulk for backends)**
   - Use **Testcontainers** to spin up a real Postgres per test run — not an in-memory fake or mocked repository
   - Tests exercise actual SQL, constraints, RLS, transactions — where backend bugs hide
   - Mocking the DB hides constraint/migration/query bugs that only appear against real Postgres

3. **Contract testing (Pact) — at service boundaries (the GAP)**
   - Consumer-driven: the consumer writes a test expressing what it needs → generates a contract → the provider verifies it independently
   - Only consumer-used behavior is tested, so providers can safely change anything no consumer depends on
   - Kills "deploy the world to check compatibility" — each side verifies independently in its own pipeline

4. **E2E — critical journeys only**
   - A few high-value end-to-end flows (signup → core action → result)
   - Expensive and flaky; keep the count low

5. **Set coverage thresholds by criticality**
   - Branch coverage ≥ target (default 70%); ≥ 90% on money/auth/data-integrity paths
   - **Coverage is a floor, not a goal** — high coverage with implementation-coupled assertions gives false confidence; chase untested *behavior*

5b. **Assert behavior, not implementation**
   - Test what the system *does* (returns this DTO, persists this row, emits this event), not how it does it internally (which function got called)
   - Tests that break on refactor — when behavior didn't change — are testing the wrong thing

5c. **Keep tests deterministic (flake kills suites)**
   - **No real time**: control `Date.now()` / clocks via fake timers; never `sleep()` to "wait for" something
   - **Real DB per test, clean state**: Testcontainers spin-up + truncate/transaction-rollback between tests; isolation > leftovers
   - Control randomness (seeded `Math.random` / `crypto`), wallclock, and the network (mock outbound or use Testcontainers for downstream too)
   - Tests pass in isolation AND in any order — no shared mutable state

6. **Validate (validation loop)**
   - Run the suite; if branch coverage < target on critical logic → add tests and re-run until met
   - For contract tests: change the provider in a way no consumer uses → verify contracts still pass (proves the contract captures only real dependencies)

## Anti-patterns

| ❌ Anti-pattern | ✅ Correct |
|---|---|
| Mocking the database in tests | Real Postgres via Testcontainers |
| All unit tests, no integration | Integration-heavy for backends (bugs live in I/O) |
| Deploy-the-world to check service compat | Consumer-driven contract tests (Pact) |
| Many flaky E2E tests | Few critical-journey E2E; integration for the rest |
| Uniform coverage target | Higher bar on money/auth/data-integrity paths |
| Asserting "function X was called with args Y" | Assert observable behavior (return value, DB row, emitted event) |
| Real time / `sleep(n)` / random clocks | Fake timers + seeded random + controlled DB state |
| Tests passing alone but failing in suite | No shared mutable state; pass in any order |

## Severity tiers

| Tier | Examples | Action SLA |
|---|---|---|
| **Critical** | 0% coverage on payment/auth/data-integrity logic; no integration test against real DB; services with no contract tests breaking each other | Fix immediately |
| **Major** | DB mocked in integration tests; business logic < 50% branch coverage | Fix this sprint |
| **Minor** | Missing contract test on a low-risk internal boundary; flaky E2E | Schedule within 2 sprints |

## Completion Criteria
- [ ] Pure logic unit-tested
- [ ] Integration tests run against real Postgres (Testcontainers)
- [ ] Contract tests on service boundaries (Pact)
- [ ] Branch coverage ≥ target (≥90% critical paths)
- [ ] All gated in CI; no skipped tests in main

## Output
- **Test suites**: unit / integration (Testcontainers) / contract (Pact) / few E2E
- **Coverage report** + CI gate
- **Commit format**: `test(<scope>): integration tests for <feature>` / `test(contract): Pact for <consumer>↔<provider>`

## Implementation

### TypeScript + NestJS + Postgres (default)
- Runner: Vitest or Jest
- Integration: **Testcontainers** (`@testcontainers/postgresql`) → real Postgres; run migrations; test against it
- Contract: **Pact** (`@pact-foundation/pact`) — consumer tests generate pacts; provider verification in its pipeline
- Coverage gate in CI (`--coverage` with threshold)

### Other stacks
- **Python / FastAPI**: pytest + `testcontainers-python`; `pact-python` for contracts
- **Go**: stdlib `testing` + `testcontainers-go`; `pact-go`
- **Universal**: Testcontainers (Docker-based) and Pact (polyglot) are cross-language; the pyramid + real-DB principle is universal

## Related skills
- `schema-design` — integration tests run against the real schema
- `api-contract` — contract tests verify the API contract holds
- `cicd-pipeline` — tests + contract verification gate the pipeline

## Reference
- **Key insight encoded**: Only consumer-used behavior is tested by a contract, so providers can safely change anything no consumer depends on — kills the need to deploy-the-world to validate compatibility. Integration tests run against a real DB, not mocks.

