Unit vs Integration vs E2E Testing (The Testing Pyramid)
Unit, integration, and end-to-end tests each check the system at a different zoom level, trading speed for realism. The Testing Pyramid, from Mike Cohn and Martin Fowler, tells you how to mix them: many fast unit tests at the base, fewer integration, a few slow E2E on top.
Ask three engineers what a "test" is and you may get three different pictures: one imagines a tiny function called with a fake input, one imagines a service talking to a real database, and one imagines a browser clicking through a checkout. They're all right — they're just describing different test levels. Unit, integration, and end-to-end tests aren't competing philosophies; they're three zoom settings on the same system, and each one sees bugs the others are blind to.
The hard part isn't understanding the levels individually — it's knowing how many of each to write. Test at too fine a grain and you'll ship code where every unit works but nothing works together; test only at the coarsest grain and you'll drown in slow, flaky suites that take twenty minutes to tell you a button moved. This guide defines each level by what it tests, how fast it is, what it catches, and what it misses, then uses the Testing Pyramid — the heuristic popularised by Mike Cohn and sharpened by Martin Fowler — to tell you how to balance them.
The three levels of testing
Unit tests verify one function or component in isolation with its dependencies mocked; integration tests verify several real units working together, like a service and its database; end-to-end tests drive the whole running app through the real UI as a user would. Scope, realism, runtime, and fragility all grow as you move up.
Unit tests — one piece, in isolation
A unit test exercises the smallest testable piece of code — a single function, method, or component — with its dependencies replaced by mocks or stubs so nothing else can interfere. Because it touches no network, no database, and no file system, it runs in milliseconds, which means you can run thousands of them on every save and every commit.
What it catches: logic bugs. Off-by-one errors, a wrong conditional, a mishandled null, a currency-rounding mistake — anything that lives inside one unit's own reasoning. When a unit test fails, it points at almost the exact line, because nothing else was in scope. What it misses: everything about how units combine. A unit test can prove your calculateTotal() is correct and your chargeCard() is correct while the two of them pass money in the wrong units to each other, and no unit test will ever notice. Example tooling: Jest, Vitest, JUnit, pytest, Go's built-in testing package.
Integration tests — units working together
An integration test takes several real units and checks that they cooperate across the seams between them. Instead of mocking the database, you run against a real (often ephemeral) one and assert that your repository layer actually reads back what it wrote. Instead of stubbing the API, you mount a component together with its real data-fetching layer and check the two speak the same shape. The units are no longer alone; the test is about the contract where they meet.
What it catches: wiring and contract bugs — the mismatches that unit tests structurally cannot see. A renamed database column, a serializer that emits snake_case while the client expects camelCase, a transaction that isn't actually committing, an API that returns 200 with an empty body. These are the failures that live between correct components. The cost: integration tests are slower (they do real I/O) and need real setup — a database to spin up, fixtures to seed, state to tear down. What it misses: the full user journey through the actual interface. Example tooling: Testcontainers, Supertest, React Testing Library against a real API layer, Spring's @SpringBootTest.
End-to-end (E2E) tests — the whole app, as a user
An end-to-end test drives the entire running application the way a real person would: it opens a browser, clicks buttons, fills forms, and asserts on what actually appears on screen — with every real layer underneath, from the UI down through the API, services, and database. Nothing is mocked. It's the only level that answers the question that actually matters to your users: can someone complete this journey?
What it catches: real user-journey failures that no lower level can — a broken route, a button that renders but doesn't wire up its handler, a login flow that fails only when the real session cookie and the real redirect meet. If sign-up is broken end to end, an E2E test is what tells you before your customers do. The price is steep: E2E tests are the slowest (seconds to minutes each) and by far the most brittle, because they depend on timing, animations, network latency, test data, and sometimes third-party services — any of which can make a test fail without a real bug behind it. That flakiness is why E2E tests are the ones teams fight with most; see our guide on debugging flaky tests. Example tooling: Playwright, Cypress, Selenium.
The three levels side by side
| Feature | Unit | Integration | E2E |
|---|---|---|---|
| Scope | One function or component, in isolation | A few real units wired together | The whole app through the real UI |
| Speed | Milliseconds — run constantly | Seconds — real I/O | Seconds to minutes — slowest |
| What it catches | Logic bugs inside one unit | Contract / wiring bugs between modules | Broken real user journeys |
| Brittleness | Very stable | Moderate — needs real setup | High — timing, network, flake |
| How many you want | Many (the base) | Fewer (the middle) | Few (the tip) |
| Example tools | Jest, Vitest, JUnit, pytest | Testcontainers, Supertest, RTL | Playwright, Cypress, Selenium |
The Testing Pyramid: how to balance them
Knowing the three levels is only half the picture; the other half is proportion. The Testing Pyramid is the mental model for that. Picture a triangle divided into three horizontal bands. The wide base is unit tests — you want many of them, because they're fast and stable, so most of your coverage should come from here. The narrower middle is integration tests — fewer, covering the risky seams where modules meet. The small tip is E2E tests — few, covering only your most critical end-to-end journeys.
The shape isn't arbitrary; it's an economic argument. As you climb, every test gets slower to run, more brittle, and more expensive to maintain — so you want the bulk of your safety net woven from the cheap, reliable threads at the bottom, and only a handful of the expensive full-system checks at the top. The practical rule of thumb: push every check as far down the pyramid as it can meaningfully go. If a bug can be caught by a fast unit test, don't spend a slow, flaky E2E test to catch it.
The ice cream cone anti-pattern
Turn the pyramid upside down and you get the ice cream cone: a huge scoop of E2E tests on top, a thin layer of integration below, and almost no unit tests at the bottom — often topped with a big blob of slow manual testing. It's what teams drift into when they test only through the UI because that's the layer they can see.
A heuristic, not a law
The pyramid is a guideline, not a physical constant, and it pays to hold it loosely. The boundaries between levels are genuinely fuzzy — Martin Fowler notes that even "unit test" means different things to different people (the solitary unit test mocks all collaborators; the sociable one lets real ones participate), so where a test sits on the pyramid is partly a matter of definition.
For some codebases a different shape fits better. The testing trophy, popular in front-end circles, deliberately fattens the integration layer — the argument being that for UI-heavy apps, tests that render a component with its real hooks and data give the best confidence-per-second, so integration, not unit, deserves the widest band. That's a reasonable position, not a heresy. The through-line across every model is the same: favour fast, reliable tests over slow, brittle ones, and spend your slow-test budget only where it buys real-world confidence you can't get any cheaper. Use the pyramid to think, not to obey.
When an E2E test fails, capture the failure
Here's where the top of the pyramid meets daily reality. An E2E test is the one most likely to fail in CI — and also the hardest to debug, precisely because it spans every layer. A red line in your pipeline tells you a journey broke, but not why. Was it a genuine bug, or was it flake? What was on the screen? What did the console say? Which network request came back wrong? Without that, you're stuck re-running the job locally and hoping it fails the same way twice — which, for a flaky E2E test, it often won't.
That's the gap BugMojo closes. When an E2E test fails, a captured session replay of the exact run — plus the console logs and network requests at the moment it broke — turns a bare red CI line into a reproducible bug. Instead of a stack trace and a shrug, a developer (or an AI agent over MCP) opens the failure and watches the journey break: the click that did nothing, the request that 500'd, the error the browser swallowed. The flakiest, most expensive layer of your pyramid becomes the one you can actually diagnose. For the level below, on framing checks as repeatable experiments, see how to write test cases; to fit all three levels into a release, see what a test plan is and how a smoke suite differs from a regression suite.
When an end-to-end test goes red in CI, capture the session replay, console, and network in one artifact — attached and ready for a developer or an AI agent over MCP.
Install the free extensionFrequently asked questions
Frequently asked questions
Sources
- Martin Fowler — 'The Practical Test Pyramid', the canonical explanation of test levels and why the distribution matters — martinfowler.com (2026)
- Martin Fowler — 'UnitTest', on what actually counts as a unit test and the sociable-vs-solitary distinction — martinfowler.com (2026)
- Google Testing Blog — 'Just Say No to More End-to-End Tests', the case for fewer E2E tests and more unit coverage — Google Testing Blog (2015)
- ISTQB Glossary — the standard definitions of 'test level', 'component testing', 'integration testing', and 'system testing' — ISTQB (2026)
Get bug-tracking insights, weekly.
Engineering deep-dives, QA playbooks, and honest tool comparisons. No spam — unsubscribe in one click.

