Need the #1 custom application developer in Brisbane?Click here →

Testing

End-to-End Testing

9 min readLast reviewed: March 2026

End-to-end (E2E) tests simulate a real user navigating your application in a browser. They click buttons, fill forms, navigate pages, and verify the UI displays correctly. E2E tests run the entire stack—frontend, backend, database—and give the highest confidence that your application works from a user's perspective. But they're also the slowest and most expensive to maintain, so use them strategically.

What E2E Tests Do

An E2E test automates what a human user would do:

  1. Navigate to the app
  2. Click a button or link
  3. Fill in a form
  4. Submit the form
  5. Verify the result (new page loads, data displays, notification appears)

E2E tests interact with the real UI. They don't know about component state or props; they only know what the user sees. This makes them remarkably similar to how humans test: click, observe, verify. But they run at computer speed, thousands of times, consistently.

Playwright vs Cypress

The two dominant E2E testing tools are Playwright and Cypress. Both automate a real browser, but they have different philosophies:

AspectPlaywrightCypress
ArchitectureOut-of-process; runs separately from the browserIn-process; runs JavaScript inside the browser
Browsers supportedChromium, Firefox, WebKit (covers all browsers)Chromium, Firefox, Edge (webkit support limited)
SpeedVery fast (can run many tests in parallel)Slower (serial execution, more overhead)
DebuggingCLI and trace viewer (powerful but steeper learning curve)Interactive test runner (very beginner-friendly)
Learning curveModerate (more verbose API)Shallow (intuitive, great docs)
CostFree, open sourceFree with optional paid cloud dashboard
Network controlExcellent (can mock APIs easily)Good but less powerful
Best forLarge teams, multi-browser, CI/CD focusTeams learning E2E, fast iteration, single app

For most projects, Playwright is the more powerful choice. But if you're new to E2E testing, Cypress's excellent developer experience and interactive test runner make it a great starting point.

Writing an E2E Test

A simple E2E test example (pseudocode):

// Navigate to the app
await page.goto('http://app.test')

// Find and click the login button
await page.click('button:has-text("Login")')

// Fill in the email field
await page.fill('input[type="email"]', 'user@example.com')

// Fill in the password field
await page.fill('input[type="password"]', 'password123')

// Submit the form
await page.click('button:has-text("Sign In")')

// Verify we're logged in (check page title or dashboard text)
await expect(page).toHaveTitle(/Dashboard/)
await expect(page.locator('h1')).toContainText('Welcome, User')

The test is readable: someone without technical knowledge can understand what it's testing. This is a major advantage of E2E tests—they serve as documentation and can be written collaboratively with non-technical stakeholders.

The Page Object Model Pattern

E2E tests are fragile. If you change a button's class name, tests break. The Page Object Model (POM) pattern encapsulates page interactions into objects, making tests more maintainable.

Instead of scattering selectors through tests, you create a LoginPage object that knows how to log in:

class LoginPage {
  constructor(page) { this.page = page }

  async goto() { await this.page.goto('/login') }
  async fillEmail(email) {
    await this.page.fill('input[name="email"]', email)
  }
  async fillPassword(pwd) {
    await this.page.fill('input[type="password"]', pwd)
  }
  async submit() {
    await this.page.click('button:has-text("Sign In")')
  }
  async login(email, pwd) {
    await this.goto()
    await this.fillEmail(email)
    await this.fillPassword(pwd)
    await this.submit()
  }
}

// In a test:
const loginPage = new LoginPage(page)
await loginPage.login('user@example.com', 'password')
await expect(page).toHaveTitle(/Dashboard/)

Now if the email input changes selector, you update LoginPage once. All tests using it work again. This scales to complex applications.

Tip
Selector strategy: Avoid brittle selectors like CSS classes that change frequently. Prefer semantic HTML: test IDs (data-testid), form labels, button text. These are stable and meaningful.

Test Flakiness and How to Reduce It

E2E tests are notorious for flakiness: they pass sometimes, fail other times, without code changes. Common causes:

  • Timing issues: Test tries to click a button before it's visible. Test expects data before it loads.
  • Network latency: API response takes longer than expected. Test times out.
  • Random data: Test data is randomized. One run uses data that exists; another doesn't.
  • Shared state: One test modifies data another test depends on.
  • Environmental differences: CI runs the test differently than local machines.

Solutions:

  • Use explicit waits: Instead of "wait 2 seconds", wait for an element to be visible or a condition to be true.
  • Control test data: Use factories to create known test data. Clean up after each test.
  • Isolate tests: Each test should be independent. Don't rely on the order of test execution.
  • Mock slow endpoints: If an API call takes time, mock it in tests. Only test with real APIs occasionally.
  • Retry flaky tests: Some flakiness is unavoidable. Retry failed tests once; if it passes the second time, it was likely a timing issue, not a code bug.

The mantra: explicit over implicit. Don't wait a fixed time; wait for an actual condition. This makes tests stable and fast.

Running E2E Tests in CI

E2E tests need a running application. In CI, you typically:

  1. Start the application (or deploy to a test environment)
  2. Run E2E tests against it
  3. Clean up test data
  4. Generate a report (video, screenshots on failure)

Many teams run E2E tests on a schedule (nightly) rather than every commit because they're slow. Critical user journeys might run on every PR; less critical journeys on schedule. Find the balance for your team.

Visual Regression Testing

E2E tests verify behavior (clicking, navigation, form submission). Visual regression tests verify appearance: does the page look right? Tools like Percy and Chromatic take screenshots, compare them to baseline images, and flag visual changes.

This catches subtle CSS bugs: a button moved 5 pixels, text is cut off, color is wrong. Visual tests complement functional tests. Together, they verify the app both works and looks correct. Many teams integrate visual tests into their CI pipeline to prevent accidental style regressions.

Note
Recording and replaying: Some tools can record user sessions and replay them, useful for debugging. But recording isn't your primary testing method. Write tests explicitly; recording is helpful for analysis.

E2E Test Environments

E2E tests run against different environments depending on their purpose:

  • Local: Developer runs tests locally during development against localhost.
  • Staging: A copy of production where you test before deploying. Most realistic.
  • Production: Smoke tests run against the live app post-deployment to ensure nothing is fundamentally broken.

Most comprehensive E2E tests run against staging. It's realistic but isolated from users. Production smoke tests are quick sanity checks (can you load the homepage?) not comprehensive E2E suites.

The Cost of E2E Test Maintenance

E2E tests are expensive to maintain:

  • Slow: A test suite can take 30+ minutes to run. CI feedback is slow.
  • Flaky: Timing issues cause false failures. Developers lose trust in the tests.
  • Tightly coupled: UI changes break tests even if functionality didn't change.
  • Hard to debug: When a test fails, figuring out why can take time. You need screenshots, videos, logs.

The testing pyramid exists for this reason: E2E tests are valuable for critical paths but shouldn't be your primary testing tool. Use unit tests for logic, integration tests for APIs, and E2E tests only for key user journeys.

What E2E Tests Are Good For

Use E2E tests for:

  • Critical user journeys: Sign up, log in, make a purchase, submit an important form.
  • Cross-browser compatibility: Test that your app works in Chrome, Firefox, Safari (different browsers behave differently).
  • Navigation flows: Verify that following links and navigation works end-to-end.
  • Multi-step processes: Checkout flow, wizard forms, complex workflows.
  • Accessibility: Verify keyboard navigation, screen reader compatibility (some aspects are hard to test with unit tests).

What E2E Tests Are Not Good For

Don't use E2E tests for:

  • Every UI variant: If you have 10 button styles, use unit tests. E2E tests are too slow.
  • Business logic: Test the tax calculation logic with unit tests, not E2E tests.
  • Error handling: Testing that an error message displays is better done with unit/integration tests.
  • Data validation: Input validation is fast to test with unit tests. E2E tests are overkill.

Critical User Journey Testing

A practical approach: identify your critical user journeys (paths users take to accomplish their main goals) and write E2E tests only for those. For an e-commerce app: sign up, search products, add to cart, checkout, pay. These are critical. Testing every color variant or filter combination is not critical; use unit tests.

This keeps your E2E test suite small (5-15 tests) and fast (minutes, not hours). You get high confidence in the most important parts without the maintenance burden of hundreds of E2E tests.

Developer Insight
E2E testing strategy: Write E2E tests for critical user journeys only. Use Playwright or Cypress. Use the Page Object Model for maintainability. Mock slow or external APIs. Wait for elements explicitly. Expect tests to take minutes, not hours. If your E2E suite is slow and flaky, you probably have too many tests. Focus on the critical paths.

Key Takeaways

E2E tests simulate real users and give the highest confidence that your app works end-to-end. They're slow and expensive to maintain, so use them strategically for critical user journeys only. Choose Playwright for power and multi-browser support, or Cypress for ease of learning. Use the Page Object Model to keep tests maintainable. Make tests stable with explicit waits. Remember the testing pyramid: many unit tests, fewer integration tests, few E2E tests. This balance gives you confidence and speed.