Unit vs Integration vs E2E: What to Actually Test
Teams that chase 100% unit test coverage often have less confidence in their app than teams with a well-balanced test pyramid. Here's why coverage percentage isn't the goal.

"How much should I test" gets answered with a coverage number when the better question is "what kind of test actually catches this kind of bug." Different test types catch fundamentally different problems — mixing them up wastes effort and gives false confidence.
Unit tests: does this one function/component do what it claims, in isolation
// Testing a single function, no dependencies
function calculateDiscount(price, percent) {
return price - (price * percent / 100);
}
test('calculates 20% discount correctly', () => {
expect(calculateDiscount(100, 20)).toBe(80);
});
- What they catch: logic errors in a specific function or component, isolated from everything else.
- What they can't catch: whether that function actually gets called correctly by the rest of the app, or whether its output is used correctly downstream.
- The trap: 100% unit test coverage tells you every function works correctly in isolation — it says nothing about whether they work correctly together.
Integration tests: do multiple pieces work together correctly
// Testing a form component + its validation + submission logic together
test('shows error and blocks submission with invalid email', async () => {
render(<SignupForm />);
fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'not-an-email' } });
fireEvent.click(screen.getByText('Submit'));
expect(await screen.findByText('Please enter a valid email')).toBeInTheDocument();
});
- What they catch: bugs at the boundaries between units — a component that renders fine alone but breaks when combined with real validation logic, real state updates, real child components.
- This is usually where the highest bug-catching value per test lives — most real bugs happen at integration points, not inside a single isolated function.
E2E tests: does the actual user flow work, in a real (or close-to-real) browser
// Testing the full signup flow in a real browser
test('user can sign up and reach the dashboard', async ({ page }) => {
await page.goto('/signup');
await page.fill('[name=email]', 'test@example.com');
await page.fill('[name=password]', 'securepass123');
await page.click('text=Sign Up');
await expect(page).toHaveURL('/dashboard');
});
- What they catch: issues that only show up in the real, fully-integrated system — routing, real network requests, actual browser behavior, third-party script interactions.
- The real cost: slow to run, flaky (network timing, animation timing), and expensive to maintain — a small UI change can break several E2E tests even when nothing is actually broken.
The testing pyramid, and why the shape matters
The traditional advice: many unit tests (fast, cheap, isolated), fewer integration tests (slower, catch real bugs), even fewer E2E tests (slow, expensive, but validate the most critical user flows end-to-end).
Where teams actually get this wrong, in both directions:
- Too many E2E tests, not enough unit/integration: CI becomes painfully slow, tests get flaky, and a single UI tweak breaks a dozen tests unrelated to what actually changed.
- 100% unit coverage, weak integration/E2E: every function is individually tested, but the app still breaks in production because components don't actually work together the way the tests assumed.
A practical framework for what to actually test:
| What you're testing | Test type |
|---|---|
| Pure logic (calculations, formatters, validators) | Unit |
| A component's behavior with real user interaction | Integration |
| A critical business flow (signup, checkout, payment) | E2E, but only the critical few |
| Every possible UI variation and edge case | Usually integration, not E2E — too slow otherwise |
Try this yourself: look at your test suite's current unit/integration/E2E ratio. If it's heavily weighted toward one type, pick a recent production bug and ask which test type would have actually caught it. That answer tells you where your test suite's real gap is, more accurately than a coverage percentage does.
Takeaway: Coverage percentage measures how much code ran during tests, not how much confidence you actually have in the app. A well-balanced mix — many unit tests for logic, targeted integration tests for component interactions, a handful of E2E tests for critical flows — catches more real bugs than maximizing any single type.





