← All posts
Concepts

How much end-to-end testing do you actually need?

Too many browser tests can slow CI and increase triage work. Too few can leave important journeys unchecked. Choose the mix by the risks you need to cover and the time your team can spend running and maintaining it.

What the pyramid still gets right

The classic shape — many unit tests, fewer integration tests, a thin cap of end-to-end tests — encodes one durable truth: the cost of a test and the cost of its failure both go up as you climb. A focused unit test can run quickly and isolate a function; duration and failure clarity depend on the test. An E2E test boots a browser, walks a real user journey, and when it goes red you still have to figure out whether the bug is in your code, the test, the environment, or a third-party script that timed out.

That gradient hasn't gone away. What has changed is the authoring cost at the top of the pyramid, and that changes where the sweet spot sits. More on that below.

What E2E is uniquely good at

An end-to-end test directly addresses the question your users actually ask: can I do the thing? It exercises the real stack — routing, auth, data layer, rendered DOM, network — in one pass. That makes it uniquely good at catching a specific class of bug that lower-level tests may miss when they do not exercise the same integration:

  • Wiring failures. Every unit passes, but the button's onClick was never bound, or the API route returns 200 with an empty body. A focused integration test may also catch it; exercise the real seam.
  • Contract drift between layers. The frontend expects total_cents; the backend started sending totalCents last sprint. Mocks hide this by definition, because you wrote the mock to match your assumption.
  • The critical path. Sign up, log in, add to cart, pay. If these break, nothing else matters. A handful of E2E tests over your revenue-generating flows are worth more than a thousand tests on a date formatter.

A single E2E test that covers checkout:

import { test, expect } from '@playwright/test';

test('customer can check out with a saved card', async ({ page }) => {
  await page.goto('/cart');
  await page.getByRole('button', { name: 'Checkout' }).click();
  await page.getByLabel('Saved card ending 4242').check();
  await page.getByRole('button', { name: 'Place order' }).click();
  await expect(page.getByText('Order confirmed')).toBeVisible();
});

That one test crosses more system boundaries than a whole folder of units. That's the point — and also why you don't want a hundred of them.

Where unit tests win

E2E is a blunt instrument for logic. The moment a behavior has more than two or three meaningful branches, testing it through the UI is slow, fragile, and a poor description of intent. Business rules — pricing, permissions, validation, state machines, anything with edge cases — belong in unit tests, close to the code, where a failure names the exact input that broke.

import { describe, it, expect } from 'vitest';
import { applyDiscount } from './pricing';

describe('applyDiscount', () => {
  it('never lets a percentage discount push the total below zero', () => {
    expect(applyDiscount(50, { type: 'percent', value: 200 })).toBe(0);
  });
});

Proving that rule through a browser would mean constructing a cart, applying a coupon, and reading a rendered total — for one of a dozen cases. As a unit test it's three lines and runs instantly. The rule of thumb: if you can express the case as an input and an expected output, it's a unit test. Save E2E for "does the journey hold together."

A sane ratio

Ratios are heuristics, not laws, and the honest answer is "it depends on your blast radius." But teams tend to over-index on one extreme, so a starting point helps:

LayerRough shareWhat it covers
Unit~70%Logic, branches, edge cases, pure functions
Integration~20%Module seams, real DB/queries, API contracts
End-to-end~10%Critical user journeys, cross-layer wiring

The number that matters isn't the percentage — it's the absolute count of E2E tests. The number of critical journeys depends on your product. Enumerate them: the flows that, if broken, would generate support tickets or lose money. That list is your E2E suite. Resist the urge to E2E-test the sixth variation of a settings toggle; that's a unit or integration concern wearing a browser costume.

Two anti-patterns to watch for:

  • The ice cream cone — a fat layer of slow UI tests over a thin base of units. CI is slow, flaky, and everyone learns to re-run red builds instead of trusting them.
  • The hollow middle — lots of units, a few E2E, nothing in between. Contract drift slips through the gap because no test exercises two real modules together.

Why generation changes the cost side

Here's the part the original pyramid couldn't account for. The pyramid is fundamentally an economic argument: E2E sits at the top because each one is expensive to write and maintain. Lower that cost and the optimal shape shifts — not toward an ice cream cone, but toward covering journeys you previously skipped because the authoring budget ran out.

That's the shift worth planning for. When a real end-to-end test for a journey can be produced from a plain-language description instead of an afternoon of getByRole archaeology, the marginal journey becomes worth covering. The five-flow suite grows to the fifteen flows that genuinely matter, while still budgeting for review, data setup and ongoing maintenance.

This is where a tool like TestVibe fits the strategy rather than replacing it. You describe a behavior in natural language; it generates the Playwright test by working through your live app in an isolated cloud session, and you run that test to confirm it does what you meant. The generated output is ordinary Playwright — readable locators, real assertions — so it lives in the same suite, under the same review, as anything you'd hand-write. Generation lowers the cost of the top of the pyramid; it doesn't change what belongs there.

The takeaway

The pyramid isn't obsolete — its economics just moved. Keep logic in fast unit tests where a failure names the cause. Reserve end-to-end tests for the journeys that lose you customers, and count them deliberately rather than letting them sprawl. Then, where generation lowers authoring effort on your app, cover the handful of important flows you used to leave uncovered.

Want to evaluate generation on one important journey? Get early access, or read how AI generation works first.

Coming soon

Turn a described flow into a reviewable test.

Describe a flow in plain English. Generate a Playwright test, run it on your schedule, and review proposed fixes when your UI changes. Join the waitlist for early access.

  • Plain English in, real Playwright out
  • Runs in your CI, on your schedule
  • No credit card, unsubscribe anytime