--- name: testing-backend-services description: Designs and writes tests for backend services — unit, integration with real dependencies in containers, contract tests, and minimal E2E — with deterministic, parallel-safe practices. Use when the user asks to test an API or service, write unit/integration/contract tests, fix flaky backend tests, set up testcontainers, or decide what to mock. Do not use for frontend or UI testing, or for load and performance testing. --- # Testing Backend Services ## When to use / when NOT to use - **Use for:** test strategy and test code for services and APIs: unit, integration, contract, E2E; de-flaking; test data design. - **Do NOT use for:** browser/UI testing, or load/perf/soak testing — different tooling and goals. ## Core rules 1. **Shape tests as a pyramid.** Many milliseconds-fast unit tests on pure logic; a solid layer of integration tests; one happy-path E2E per critical flow. Inverting it (E2E-heavy) buys slow, flaky suites. 2. **Integration tests hit real dependencies in containers.** Spin up Postgres/Redis/broker via testcontainers. - ✅ test repository code against a real Postgres container - ❌ mock the database driver and assert SQL strings — that tests your mock. 3. **Mock only what you don't own** (third-party HTTP APIs, clocks, randomness) — and pin those mocks with contract tests where possible. 4. **Test behavior, not implementation.** Assert on outputs, state changes, and emitted events — not on which internal methods were called. Refactors must not break green tests. 5. **No sleeps.** Poll with a timeout for async effects; freeze the clock for time logic; seed randomness. A test that needs `sleep(2)` is a race you scheduled. - ✅ `wait_until(lambda: outbox.count() == 1, timeout=5)` - ❌ `time.sleep(2); assert outbox.count() == 1` 6. **Each test is independent and parallel-safe:** owns its data (unique IDs per test), never depends on execution order, cleans up via transaction rollback or per-test schema. 7. **Factories over shared fixtures.** `make_user(email=...)` with overridable defaults beats a giant `fixtures.sql` that every test secretly depends on. 8. **Contract tests guard API boundaries:** provider verifies it still satisfies consumer expectations (Pact or OpenAPI-based) on every CI run — cheaper than E2E across repos. ## Workflow 1. Classify the change: pure logic → unit; touches DB/broker/HTTP edge → integration; crosses service boundary → contract; business-critical flow → one E2E. 2. Write the test first at the lowest level that can catch the bug. 3. Build test data with factories; give every entity a per-test unique key. 4. Replace any sleep/order dependency with polling, frozen clocks, seeded RNG. 5. Validate: run the suite twice — full run and `--last-failed` in random order (`pytest -p randomly`); both must pass. Run the new test 20× (`pytest --count=20 -x`) to prove it's not flaky. ## Edge cases & failure modes - **Test passes locally, fails in CI** → almost always shared state or timing; check for fixed ports, shared DB rows, real clock usage. - **Container startup dominates runtime** → reuse one container per session with per-test transactions/schemas, not one container per test. - **Untestable code** (network calls in constructors, global singletons) → refactor for injection first; don't monkey-patch around design problems. - **Non-determinism sources** (UUIDs, now(), env) → inject them; asserting on wall-clock values is a flake factory. ## References Deeper recipes and gotchas: see [references/patterns.md](references/patterns.md)