#  Unit vs Integration vs E2E: What to Actually Test


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

```javascript
// 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**

```javascript
// 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**

```javascript
// 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.
