The Testing Pyramid
What to test at which level, and why 100% unit coverage can still ship a broken app.
30 minDifficulty 2/5testingAI-writtenWritten by a model on 16 August 2026 and not yet read by a person. Checked automatically: schema, the pedagogical rules the hand-written material is held to, and every diagram parsed for real.
Before this
Why this exists
Every function has a unit test and every one passes, yet the app crashes on first use because the payment service and the order service disagree about the shape of a shared object. Unit tests proved each piece works alone; nothing proved the pieces work together — that's a different kind of test, checking a different kind of failure.
The mental model
Picture a pyramid. At the base, many fast unit tests check individual functions in isolation. In the middle, fewer integration tests check that two or three real pieces cooperate correctly — a service and a real database, say. At the top, a handful of end-to-end tests drive the whole system like a real user. Each layer catches what the layer below it structurally cannot.
How it works
A unit test isolates one thing by replacing everything around it
Testing `calculateShipping` in isolation means replacing its dependencies — a rates API, a database — with fakes that return known values. This makes the test fast and deterministic, but it also means the test can't tell you whether the real rates API actually returns data in the shape `calculateShipping` expects.
Integration tests catch the seams
An integration test runs two or more real pieces together — the actual database, not a fake — and checks they cooperate correctly. This is where you catch a mismatched schema, a wrong SQL query, or an API contract that changed underneath a caller, none of which a unit test with fakes can see.
The pyramid shape is about cost, not importance
Unit tests are cheap — milliseconds, no external dependencies, easy to write many of. Integration tests cost more — real infrastructure, slower, sometimes flaky from network variance. End-to-end tests cost the most — a full deployed environment, browser automation, minutes per run. You want many of the cheap ones and few of the expensive ones, each catching what only it can.
100% unit coverage says nothing about the wiring
Every function can be individually correct while the way they're connected is wrong — a return value with a field named `total` being read as `totalAmount` by the caller, say. This is the specific failure mode the pyramid's middle and top layers exist for, because no amount of testing functions alone catches how they're plugged together.
The mechanism
Confidence builds upward. Unit tests give fast feedback on individual logic during development. Integration tests, run less often, confirm the pieces those units belong to actually cooperate. End-to-end tests, run least often, confirm the deployed system as a whole behaves correctly for a real user journey.
flowchart BT U[Unit tests\nmany, fast, isolated] --> I[Integration tests\nfewer, real dependencies] I --> E[End-to-end tests\nfewest, full system]
What people get wrong
- More end-to-end tests always mean a more thoroughly tested system.
- End-to-end tests are slow and often flaky due to timing and environment variance; a suite dominated by them becomes slow to run and unreliable to trust, which is why the pyramid inverts the naive intuition. Teams new to testing often over-invest in end-to-end coverage because it feels the most 'real', then suffer from a test suite that takes an hour and fails randomly for reasons unrelated to actual bugs.
- Unit tests and integration tests are interchangeable — just a difference in how many pieces you're testing.
- They test fundamentally different failure classes: unit tests catch logic errors within a piece, integration tests catch contract mismatches between pieces — neither substitutes for the other. A codebase with excellent unit coverage but no integration tests will still ship bugs where correctly-tested pieces don't actually fit together.
- If a function calls an external API, you can't unit test it.
- You can unit test the function's logic by replacing the API call with a fake that returns known values — you're testing the function's behaviour given that response, not the API itself. Confusing 'testing the function' with 'testing the whole chain including the API' leads people to either skip unit tests entirely or accidentally write slow integration tests labelled as unit tests.
When not to use it
- The logic under test has no external dependencies at all — a pure calculation.
- A unit test is sufficient and appropriate; there's no seam to integration-test because there's nothing external to disagree with.
- You need confidence a critical user flow — checkout, sign-up — genuinely works end to end after a deploy.
- A small number of end-to-end tests covering only the highest-value flows, per `e2e-testing` — not an attempt to cover every path this way, which doesn't scale.
Terms
- Testing pyramid
- — A model recommending many fast unit tests, fewer integration tests, and the fewest end-to-end tests, based on relative cost and speed.
- Contract
- — The agreed shape of data or behaviour between two pieces of a system — what one side promises to provide and the other assumes it will receive.
- Test double
- — A generic term for a fake, stub, or mock standing in for a real dependency during a unit test.
- Seam
- — The boundary between two components where they exchange data or calls — exactly where integration tests focus, since it's where mismatches hide.
In an interview
Your unit test suite is green but a bug reached production from two services disagreeing on a field name. What test would have caught this, and why didn't the unit tests?
- an integration test running both real services together would catch a field-name mismatch
- unit tests use fakes that were written with the same (possibly wrong) assumption as the code, so they agree with the bug
Can you recall it?
What specific class of bug does an integration test catch that a unit test structurally cannot, and why?