The testing pyramid tells us to write many unit tests, fewer integration tests, and even fewer end-to-end tests. This advice is often misinterpreted as “e2e tests are bad.” They’re not. They test something nothing else can: whether your application actually works for users.

What E2E Tests Catch

I’ve seen codebases with 95% unit test coverage that were completely broken. How? Because:

  • The API returned data in a format the frontend didn’t expect
  • A CSS change made the submit button unclickable
  • A third-party script blocked the critical path
  • The login flow worked, but the redirect was wrong
  • Feature flags were configured differently in production

Unit tests mock away these integration points. E2E tests expose them.

The User’s Perspective

Unit tests verify implementation. E2E tests verify behavior.

Consider a checkout flow:

// Unit test: verifies calculateTotal function
test('calculates total with tax', () => {
    expect(calculateTotal(100, 0.08)).toBe(108);
});

// E2E test: verifies checkout actually works
test('user can complete purchase', async () => {
    await page.goto('/products');
    await page.click('[data-test="add-to-cart"]');
    await page.click('[data-test="checkout"]');
    await page.fill('[data-test="card-number"]', '4242424242424242');
    await page.click('[data-test="submit"]');
    await expect(page.locator('[data-test="confirmation"]')).toBeVisible();
});

The unit test passes even if the checkout page is completely broken. The E2E test fails if anything in the entire flow is broken—exactly what you want to know before deploying.

Critical User Journeys

You don’t need E2E tests for everything. Focus on critical user journeys—the paths users must be able to complete for your product to function:

E-commerce:

  • Browse → Add to cart → Checkout → Confirmation
  • Account creation → Email verification → Login

SaaS:

  • Signup → Onboarding → Core feature usage
  • Login → Dashboard → Settings

Content platform:

  • Search → View content → Engage (like/comment/share)
  • Create → Publish → Verify visibility

If these flows break, your business breaks. They deserve E2E coverage.

E2E Tests as Documentation

Well-written E2E tests document how your application is supposed to work:

describe('Project Management', () => {
    test('team lead can create project and invite members', async () => {
        await loginAs('team-lead@example.com');
        await page.click('[data-test="new-project"]');
        await page.fill('[data-test="project-name"]', 'Q1 Initiative');
        await page.click('[data-test="create"]');

        await page.click('[data-test="invite-member"]');
        await page.fill('[data-test="email"]', 'developer@example.com');
        await page.selectOption('[data-test="role"]', 'contributor');
        await page.click('[data-test="send-invite"]');

        await expect(page.locator('[data-test="pending-invites"]'))
            .toContainText('developer@example.com');
    });
});

This test is also a specification. New team members can read it to understand the feature. Product managers can verify it matches requirements. QA can trace manual testing to automated coverage.

Catching Regressions Before Users Do

Every deployment is a risk. E2E tests running in CI are your safety net:

# GitHub Actions example
e2e-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Start application
        run: docker-compose up -d
      - name: Wait for healthy
        run: ./scripts/wait-for-healthy.sh
      - name: Run E2E tests
        run: npx playwright test
      - name: Upload failure artifacts
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/

When tests fail, you see exactly what broke—often with screenshots and traces—before any user is affected.

Visual Regression Testing

E2E tests can catch visual bugs that unit tests never could:

test('homepage renders correctly', async () => {
    await page.goto('/');
    await expect(page).toHaveScreenshot('homepage.png');
});

This catches:

  • CSS regressions
  • Layout shifts
  • Missing assets
  • Font loading issues
  • Responsive design breaks

When the screenshot doesn’t match the baseline, you see exactly what changed.

Cross-Browser Confidence

Your unit tests run in Node.js. Your users run Chrome, Firefox, Safari, Edge, and mobile browsers. E2E tests can verify all of them:

// playwright.config.js
module.exports = {
    projects: [
        { name: 'chromium', use: { browserName: 'chromium' } },
        { name: 'firefox', use: { browserName: 'firefox' } },
        { name: 'webkit', use: { browserName: 'webkit' } },
        { name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
        { name: 'mobile-safari', use: { ...devices['iPhone 12'] } },
    ],
};

That flexbox layout that works in Chrome but breaks in Safari? The E2E test catches it.

The Confidence to Deploy

The real value of E2E tests is confidence. Confidence to:

  • Deploy on Friday afternoon
  • Refactor critical paths
  • Upgrade dependencies
  • Onboard new developers who make changes immediately

Without E2E tests, every deployment is a leap of faith. With them, you have evidence that your application works.

The Tradeoffs

E2E tests aren’t free:

Slower: Minutes instead of milliseconds.

Flakier: Browser automation is inherently less stable than function calls.

More maintenance: UI changes require test updates.

Infrastructure: You need browsers and possibly real backends running.

These are real costs. But for critical user journeys, the benefits outweigh them.

A Balanced Strategy

  1. Unit tests: Cover business logic, utilities, and components in isolation
  2. Integration tests: Verify API contracts and service interactions
  3. E2E tests: Protect critical user journeys

Don’t skip the top of the pyramid because it’s harder. The view from up there—confidence that your application actually works—is worth the climb.