Skip to content

E2E Testing Concepts

When to Use

When you need to verify that a complete user journey works correctly through a real browser, real JavaScript execution, and real application stack. E2E tests are the most expensive tests to write, run, and maintain — use them sparingly, only for critical journeys where no cheaper test gives adequate confidence.

What E2E Tests Are For

E2E tests verify the integrated system from the user's perspective: - Real browser (Chromium, Firefox, WebKit) executes JavaScript - Real network requests hit a real (or realistic) server - Real CSS, rendering, layout, and interaction events occur - Client-side behavior (form validation, modals, AJAX updates) is exercised

This is the only layer that catches: - CSS/JS conflicts that break interactive behavior - Network request failures that only appear in a real browser context - Client-side routing errors in SPAs - Cross-browser rendering or event handling differences - Third-party script interference

The Cost of E2E Tests

E2E tests are 10–100× slower than integration tests. Each test typically: - Launches a browser process - Navigates to a URL (network + render) - Interacts with DOM elements (potentially with timing dependencies) - Can be flaky due to animation, network jitter, timing, or state pollution

An E2E suite with 500 tests can take 30+ minutes to run, which pushes developers to skip it. Keep E2E tests few. Cover critical paths only.

What to Cover with E2E Tests

Cover in E2E: - The most critical user journeys (sign-up, log-in, purchase, content creation) - Flows that require JavaScript interaction (AJAX forms, rich text editors, drag-and-drop) - Critical integration with third-party scripts (analytics, payment widgets) - Smoke tests that verify the app is alive after deployment

Do NOT cover in E2E: - Logic variants and edge cases (test those with unit tests) - Every page in the site (smoke a representative sample) - Things already covered by integration tests - Error states that require mocking (flaky to induce in E2E)

Pattern

// Playwright E2E: critical user journey
// One test per journey; no logic variants here

test('user can complete checkout', async ({ page }) => {
  await page.goto('/products/widget');
  await page.click('[data-testid="add-to-cart"]');
  await page.goto('/cart');
  await page.click('[data-testid="checkout"]');
  await page.fill('[name="email"]', 'alice@example.com');
  await page.fill('[name="card"]', '4242424242424242');
  await page.click('[data-testid="pay"]');
  await expect(page.locator('[data-testid="confirmation"]')).toBeVisible();
});

Keep E2E tests declarative and high-level. If you find yourself testing logic details in E2E, move those cases down the stack.

Common Mistakes

  • Using E2E as the primary test type → Enormously expensive suite, slow CI, high flakiness, low maintainability
  • Testing every edge case in E2E → These belong in unit/integration; E2E verifies the happy path works end-to-end
  • No test isolation → Tests share user accounts, browser cookies, or database state; one failure cascades
  • Brittle selectors (CSS class names, positional selectors) → Use data-testid attributes or semantic role + label queries for stability
  • Not running E2E in CI with the same environment as production → Catches a different set of bugs than local runs

See Also