End-to-End Testing
Drive a real browser through the full user journey, from login to checkout, against a deployed-like environment.
Why
End-to-end tests are the only automation that verifies the user actually can complete the journey — the frontend renders, the API responds, the database persists, and the wiring between all three holds. Unit and integration tests each verify a slice, but a deploy can break the seam between frontend and backend (a renamed API field, a CORS change, a routing mismatch) and nothing but an E2E test will catch it. They are slow and flaky, which is why you write few of them — but those few guard the highest-value flows: signup, checkout, the thing that makes money.
When
Write E2E tests only for the top 3-5 critical user journeys (the ones that directly generate revenue or are required for onboarding). Add one per major regression that reached production. Run on every deploy to staging, nightly on production-like.
How
1. Choose the tool and the environment - Playwright (modern, cross-browser, auto-wait) or Cypress (dev-friendly) - Run against a staging environment with real data, not localhost mocks 2. Identify the critical journeys (3-5, no more) - Signup → verify email → first login → complete onboarding - Browse product → add to cart → checkout → payment → confirmation - The single action that 80% of revenue depends on 3. Write the test as a user would act, not as a dev would probe - Click visible buttons, type into visible fields, wait for visible text - Never use internal CSS classes or data-test IDs that bypass the UI - Add data-testid only when a selector is otherwise ambiguous 4. Handle async and flakiness - Use auto-wait (Playwright waits for elements to be actionable) - Never use fixed sleeps (sleep(2000)); wait for a condition instead - Retry flaky tests up to 2x in CI, but investigate any test that needs retries 5. Run on the right cadence - On every PR that touches the journey's files (gated by path filters) - Nightly full suite against the latest staging deploy - Never on every save — they are too slow
Example
// e2e/checkout.spec.ts
import { test, expect } from "@playwright/test";
test("user can buy a product end-to-end", async ({ page }) => {
// Login
await page.goto("/login");
await page.getByLabel("Email").fill("user@test.com");
await page.getByLabel("Password").fill("secret123");
await page.getByRole("button", { name: "Sign in" }).click();
await expect(page).toHaveURL("/dashboard");
// Browse and add to cart
await page.getByRole("link", { name: "Wireless Headphones" }).click();
await page.getByRole("button", { name: "Add to cart" }).click();
await expect(page.getByText("1 item in cart")).toBeVisible();
// Checkout
await page.getByRole("link", { name: "Cart" }).click();
await page.getByRole("button", { name: "Checkout" }).click();
await page.getByLabel("Card number").fill("4242424242424242");
await page.getByLabel("Expiry").fill("12/28");
await page.getByLabel("CVC").fill("123");
await page.getByRole("button", { name: "Pay" }).click();
// Confirm
await expect(page.getByText("Order confirmed")).toBeVisible({ timeout: 15000 });
await expect(page).toHaveURL(/\/orders\/\d+/);
});Common Problems
- ⚠E2E tests that use fixed sleeps (await sleep(3000)) to wait for loading are flaky — on a slow CI runner the load takes 3.5s and the test fails, on a fast one it wastes 3s per assertion, and the suite becomes both slow and unreliable.
- ⚠Testing every minor UI variation as an E2E test bloats the suite to 200+ tests that take 40 minutes, so the team stops running them on PRs and only finds regressions after deploy.
- ⚠Running E2E against localhost with mocked APIs passes green but misses every real-backend integration issue — the test proves the frontend works with a fake, not with the real system.
- ⚠Selectors tied to CSS classes or DOM structure (div > span:nth-child(2)) break on any harmless styling refactor, turning the E2E suite into a maintenance burden that erodes trust.
Best Practices
- ✓Use Playwright's auto-waiting and getByRole/getByLabel selectors — they survive styling changes and reflect how a real user (or screen reader) addresses the page.
- ✓Keep the E2E suite under 20 tests covering only the revenue-critical and onboarding journeys; push everything else down to integration or unit tests.
- ✓Run E2E against a staging environment with a real database and real third-party sandbox (Stripe test mode, Mailtrap), not localhost with mocks — the whole point is to test the real wiring.
- ✓Investigate every flaky E2E test within 24 hours — a flaky test that is ignored is worse than no test, because it trains the team to dismiss all red signals.