Integration Testing¶
Integration testing checks that separately built parts work together. Its target is not the logic inside a component, that is unit testing, but the seams: wiring, contracts, serialisation, transactions, protocols, and every assumption two components made about each other without checking.
graph LR
subgraph Unit["Unit tests cover this"]
A[Component A logic]
B[Component B logic]
end
A ---|integration tests cover this| B
B ---|and this| DB[(Database)]
A ---|and this| EXT[External service]
Two flavours¶
| Component integration | System integration | |
|---|---|---|
| Between | Modules inside one deployable | Separate systems or services |
| Examples | Service with repository, controller with service | Order service with payment provider, app with message broker |
| Owned by | The development team | The team plus whoever owns the other side |
| Main risk | Wiring and data mapping | Contract drift, protocol and version mismatch |
Integration strategies¶
graph TD
S[Integration strategy] --> BB[Big bang: assemble everything, then test]
S --> TD[Top down: high level first, stubs below]
S --> BU[Bottom up: low level first, drivers above]
S --> SW[Sandwich: both ends toward the middle]
S --> CI[Continuous: integrate on every commit]
| Strategy | Needs | Trade-off |
|---|---|---|
| Big bang | Nothing until the end | Failures are ambiguous, everything is suspect at once |
| Top down | Stubs for lower layers | Early demo of flows, low-level defects found late |
| Bottom up | Drivers for upper layers | Solid foundations, no visible system until late |
| Sandwich | Both stubs and drivers | Parallel work, more scaffolding to maintain |
| Continuous | Automation and a pipeline | The modern default, small integration steps so failures are attributable |
Continuous integration wins for the same reason small commits win: when only one thing changed since the last green run, the failure has one plausible cause.
What to make real¶
The central design decision of an integration test.
| Dependency | Default choice | Why |
|---|---|---|
| Your own database | Real, in a container | Schema, constraints and SQL dialect are exactly what you are testing |
| Message broker | Real or a faithful in-memory version | Ordering and delivery semantics are the risk |
| Third-party HTTP API | Simulated, plus a contract test against the real one | Speed and reliability, without pretending the contract is verified |
| Clock and randomness | Controlled | Determinism |
Replacing your own database with an in-memory substitute of a different engine defeats the purpose: the test then verifies the substitute, and the constraint violation appears in production instead.
Contract drift, the defect this level exists for¶
sequenceDiagram
participant O as Order service
participant P as Payment service
O->>P: POST /charge {amount_cents: 1000}
Note over P: Provider renames the field to "amount"
P-->>O: 400 Bad Request
Note over O: Unit tests of both sides still pass
Both sides are internally correct and the system is broken. Only a test that crosses the boundary sees it, which is why services with a shared consumer need contract testing rather than more mocks.
Keeping this level healthy¶
- Keep them fewer than unit tests and more than end-to-end tests. They are slower and less isolated than units, faster and more targeted than journeys.
- One integration per test. A test that exercises four seams reports an ambiguous failure.
- Own the test data. Each test creates what it needs and cleans up, otherwise ordering dependencies appear and the suite becomes flaky.
- Run them on every commit. An integration suite that runs nightly finds problems after several more commits have landed on top of them.
Check Your Understanding¶
Why is a big bang integration strategy usually a poor choice?
A team swaps its production database for a different in-memory engine in integration tests. What is the main problem?