Skip to main content

Command Palette

Search for a command to run...

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.

Updated
4 min readView as Markdown
 Unit vs Integration vs E2E: What to Actually Test
J
Jayesh Sojitra | AI & Frontend

"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.

30 Days of AI

Part 10 of 50

A 30-day series breaking down AI concepts, tools, and prompts in plain, jargon-free language — for beginners and professionals who want to actually understand and use AI, not just talk about it.

Up next

10 Accessibility Mistakes Frontend Devs Make

Most accessibility issues aren't from ignoring accessibility — they're from a handful of habits that quietly break it without anyone noticing during development.