Three test types that check completely different things, at completely different costs — and why the shape of your test suite matters as much as its size.
"We have good test coverage" is a claim that hides an important question: coverage of what kind, at what layer? A unit test that mocks every dependency can hit 100% line coverage on a function while never once proving that function actually works when wired to the real database it calls in production. Unit, integration, and end-to-end (E2E) tests each verify a different scope of the system — a single function in isolation, a handful of real components working together, or an entire user-facing flow through the deployed app — and each comes with a different speed and cost. Understanding that tradeoff, not just writing "more tests," is what the testing pyramid is actually describing.
Every layer up the pyramid trades isolation for realism, and realism is expensive: a unit test fakes everything around the code under test, so it's fast, deterministic, and pinpoints failures to one function — but it can't prove your database query is correct, because there's no real database involved. An integration test wires real components together (a real database, a real message queue) and catches the bugs that live at those seams, at the cost of needing that infrastructure running and taking longer to execute. An E2E test drives the actual deployed application the way a user would, which is the only layer that can catch a broken button, a misconfigured environment variable, or a CDN serving stale JavaScript — but it's slow, and because it depends on so many moving parts (network, timing, real browser rendering), it's also the layer most prone to flaking for reasons that have nothing to do with your code being wrong. The pyramid shape — many fast unit tests, fewer integration tests, very few E2E tests — exists because you want the cheap, fast layer to catch the bulk of regressions, and reserve the slow, expensive, flake-prone layer for the handful of critical user journeys where only full realism will do.
No — line or branch coverage from unit tests only proves that each piece of code behaves correctly in isolation, against the inputs your mocks handed it. It says nothing about whether those pieces are wired together correctly. A classic example: calculatePrice() can have 100% coverage and be perfectly correct, while the service that calls it passes the wrong field name from the real database row — a bug that only an integration test, running against the real schema, would ever catch. The opposite mistake is just as common and often called the "ice cream cone" anti-pattern: a suite top-heavy with slow, brittle E2E tests and almost no unit tests, which makes the suite slow to run, expensive to maintain, and hard to debug, because a single E2E failure could be caused by any layer of the stack. Good coverage means covering each layer for what it's actually good at — unit tests for logic correctness and edge cases, integration tests for the seams between real components, and a small set of E2E tests for the handful of user journeys where nothing less than the real thing will do.
Explains what each layer of the testing pyramid actually verifies — a single function in isolation, real components wired together, or a full user flow through the deployed app — and why the pyramid's shape (many fast unit tests, fewer integration tests, very few E2E tests) reflects a genuine cost and confidence tradeoff, not an arbitrary convention.
Teams often talk about "test coverage" as a single number, as if all tests verify the same kind of correctness. They don't. A unit test proves a function is correct given the exact inputs your mocks constructed — it says nothing about whether the real database, real API, or real UI actually supply those inputs the way you assumed. Treating "we have lots of tests" as equivalent to "the system works end to end" is the root of a lot of production incidents that pass a fully green test suite.
Unit tests exercise a single function, class, or module in isolation, with every external dependency (database, network, filesystem, other modules) replaced by a mock, stub, or fake. They're fast (single-digit milliseconds), deterministic, and pinpoint failures precisely — but by design they never touch the real integration points.
Integration tests wire two or more real components together — a service and a real (often test) database, or two internal modules without mocking the boundary between them — and verify that the seam between them actually works: the SQL query is valid against the real schema, the API request matches what the real endpoint expects. They're slower and need real infrastructure running, but they catch exactly the class of bug unit tests are structurally blind to.
End-to-end tests drive the fully deployed (or near-deployed) application the way a real user would — typically through a real or headless browser — covering the UI, the network, the backend, and the database in one pass. They're the slowest and most prone to flaking (timing issues, environment differences), but they're the only layer that can catch a broken button, a bad deploy config, or a UI regression that never touches the tested logic at all.
A healthy suite is pyramid-shaped for cost reasons: unit tests are cheap enough to run on every save and should cover the bulk of business logic and edge cases; integration tests should cover the handful of critical seams (database queries, external API calls, message queue contracts); E2E tests should cover only the small number of user journeys where a break would be catastrophic (checkout, login, payment) — because at dozens of seconds each and higher flake rates, a suite with hundreds of E2E tests becomes too slow and unreliable to trust or maintain. This is the practical core of test-driven development and CI/CD pipelines: fast layers gate every commit, slower layers gate releases.
It's the inverse of the testing pyramid — a suite with many slow, brittle E2E tests and few fast unit tests. It happens naturally when teams test "from the outside" because it feels more realistic, but it results in a suite that is slow to run, expensive to maintain (UI changes break tests that have nothing to do with the actual logic change), and hard to debug, because any single E2E failure could originate from any layer of the stack.
If the thing you're actually worried about is whether your code and a real external system (database, API, queue) agree on a contract — the exact query syntax, the exact response shape — write an integration test against the real thing (or a faithful test double like a local Postgres container), because more mocking just moves the risk of a wrong assumption further from where it would surface. If you're verifying business logic, edge cases, or error handling that doesn't depend on a real external system, a unit test is faster and just as reliable.
For a small number of critical user journeys, yes — nothing else catches a genuinely broken production-like flow. But because E2E tests depend on timing, real network conditions, and full browser rendering, they need deliberate flake-reduction work (explicit waits instead of fixed sleeps, retries, isolated test data) to stay trustworthy; an E2E suite that developers learn to ignore because it "fails randomly" has lost its entire value.
TDD's red-green-refactor cycle is most commonly practiced at the unit level because the fast feedback loop is what makes writing the test first practical — you can run it in milliseconds after every small change. It can be applied to integration and E2E tests too, but the slower feedback loop changes the workflow considerably, so many teams reserve strict TDD for unit tests and write integration/E2E tests to verify behavior after the fact.
There's no universal number, but a useful signal is whether the E2E suite still runs fast enough, and reliably enough, that the team actually trusts and waits for it before every release. When it starts taking so long or failing so often (for reasons unrelated to real bugs) that people begin skipping or ignoring it, that's a sign too much verification has been pushed to the most expensive layer and some of it should move down to integration or unit tests instead.
Try our Software Engineering & Cloud Studio
More calculators, simulators, and guides for this discipline.