← Software Engineering & Cloud Studio
Concept Explainer · Software Engineering

Unit vs Integration vs E2E Testing

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.

The testing pyramid: scope, speed, and count trade off together

E2Efew · slow · brittleINTEGRATIONmoderate count · moderate speedUNITmany · fast · cheap · isolatedFEWERMANYSLOWERFASTER
Unit
~1-10 ms each
Thousands can run on every save.
Integration
~100 ms-1 s each
Hundreds, run on every commit.
E2E
~5-60 s each
Dozens, run pre-release.

What each layer actually exercises, for the same feature

FEATURE: "ADD TO CART"Browser UICart servicecalculatePrice()pure functionReal databaseUNIT — this box only, inputs fakedINTEGRATION — service + real DB, UI still fakedE2E — real browser click through the real deployed stack, nothing fakedOnly E2E would have caught: the "Add to Cart" button was silently disabled by a CSS regression.Only a unit test could isolate: calculatePrice() double-applies the discount when quantity is 0.
What unit tests can't catch
Wiring & integration bugs
A mocked database can't reveal a wrong SQL query, a broken API contract, or a real network timeout.
What E2E tests can't do cheaply
Pinpoint the root cause fast
A failed checkout flow tells you something broke — a unit test tells you exactly which function and why, in milliseconds.
Why this works

The pyramid shape is a cost curve, not a rule about which layer matters most.

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.

Common misconception
"We have 100% unit test coverage, so the app works."

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.

Related Concept Explainers
Monolith vs Microservices
Read it →
Idempotent vs. Non-Idempotent Operations
Read it →
Race Conditions vs. Deadlocks
Read it →
Mocking vs Stubbing vs Faking — What Test Doubles Actually Replace
Coming soon

Unit vs Integration vs E2E Testing — Concept Explainer

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.

Why This Is Commonly Confused

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.

What Each Layer Actually Verifies

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.

Where This Matters in Practice

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.

Frequently asked questions

What is the "ice cream cone" anti-pattern?

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.

Should I write an integration test or a unit test with more mocks?

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.

Are E2E tests worth the flakiness?

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.

Does test-driven development (TDD) apply to all three layers?

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.

How many E2E tests is too many?

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.

Related tools & guides

Software Testing & TDD: The Testing Pyramid, TDD, and What Actually WorksSDLC & Agile Methodology GuideCI/CD with GitHub ActionsSystem Design Interview Guide