Testing Strategy — Detailed#
Layers#
| Layer | What it covers | Speed | Brittleness |
|---|---|---|---|
| Unit | one function / class in isolation | ms | low |
| Component | one service end-to-end in-process | tens of ms | low |
| Integration | service + real DB / queue / cache | hundreds of ms | medium |
| Contract | API consumer ↔ provider expectations | ms | low |
| E2E | browser / mobile through whole stack | seconds-minutes | high |
| Performance / load | latency, throughput, soak | varies | n/a |
| Chaos | failure injection | varies | n/a |
Test doubles#
classDiagram
class Dummy {
+never used
}
class Stub {
+returns canned data
}
class Fake {
+real-ish working impl<br/>e.g. in-memory DB
}
class Spy {
+records calls
}
class Mock {
+pre-programmed expectations<br/>verifies usage
}
| Term | Behaviour | Verifies usage? | Common library |
|---|---|---|---|
| Dummy | filler argument | no | n/a |
| Stub | returns fixed values | no | hand-coded |
| Spy | records calls, default behaviour | optional | Mockito spy, jest |
| Mock | expectations programmed up-front | yes | Mockito mock, jest.mock |
| Fake | in-memory working impl | no | hand-coded (in-mem repo) |
Rule of thumb: prefer fakes, fall back to stubs, use mocks sparingly.
TDD cycle#
flowchart LR
R([Red - write failing test])
G([Green - write minimum code to pass])
REF([Refactor - clean up, keep green])
R --> G --> REF --> R
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class R,G,REF service;
Useful for: - New behaviour with a clear spec. - Bug-fix (write failing test first). - Algorithms with well-defined inputs/outputs.
Not useful for: - Exploratory UI work. - Spike code expected to be thrown away.
Contract testing#
flowchart LR
CC[Consumer test]
PACT[Pact file - JSON contract]
PV[Provider verification]
CC --> PACT --> PV
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class CC,PACT,PV service;
Pact / Spring Cloud Contract: consumer writes "I expect provider to respond like this"; provider runs the spec against its real handler. Catches API drift without coupling deploys.
End-to-end tests (judiciously)#
- Cypress / Playwright / Selenium for browsers.
- Detox / Appium for mobile.
- One smoke test per critical user journey (login, checkout, send-message).
- Run on every deploy; don't run on every PR (too slow).
Performance testing#
- Load test: target QPS, sustained, measure p99.
- Stress test: increase until failure; observe where + how.
- Soak test: 24 h+ at production rate; find leaks.
- Spike test: sudden bursts; verify autoscale + protection.
Tools: k6, Locust, Gatling, wrk, vegeta.
Coverage#
- 80% line coverage is a floor, not a goal.
- Branch coverage > line coverage in usefulness.
- 100% is over-engineering; focus on the code path that matters.
Property-based testing#
Generates many random inputs and asserts an invariant. Hypothesis (Python), fast-check (JS), jqwik (Java), QuickCheck (Haskell).
Mutation testing#
Run "mutants" of your code (flip > to <, etc.); a useful suite catches them. Tools: PITest (Java), Stryker (JS).
Anti-patterns#
- Testing the framework: don't test that Spring autowires.
- Mocking everything: brittle, low-value tests pass while the system is broken.
- One giant integration test: long, hard to debug, slow.
- Tests with sleeps: use awaitility / polling assertions.
- Snapshot tests as the only safety net: large diffs become rubber-stamped.
Glossary & fundamentals#
Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.
| Tag | Concept | What it is | Page |
|---|---|---|---|
LLD |
Testing strategy | pyramid, doubles, TDD, contracts | testing-strategy |
LLD |
Behavioural patterns | Strategy, Observer, State, Command, Chain | behavioral-patterns |