How would you approach end to end testing with Cypress or Playwright?
E2E tests drive the real browser through the real app, hitting (usually) the real or stubbed backend. Cypress runs in-browser with a synchronous-feeling API and time-travel debugging — fast feedback, modern stack. Selenium drives the browser via WebDriver protocol — language-agnostic, broad browser support, slower and flakier. Playwright is the modern alternative to both. Trade-off: E2E catches integration bugs unit tests miss, but is slow, expensive, and flaky if poorly written. Pyramid: many unit, some integration, few E2E.
What E2E means
End-to-end tests drive the real app in a real browser, simulating what a user does — clicking, typing, navigating — and asserting on the result. They span the full stack: frontend, network, backend (often stubbed).
They're the highest-fidelity, slowest, most expensive layer of the testing pyramid.
Why E2E exists
Unit tests verify a function. Integration tests verify modules together. E2E verifies the whole system from the user's perspective:
- The build outputs the right bundle.
- Routing works.
- API calls succeed.
- State updates render correctly.
- Cross-cutting concerns (auth, feature flags) work end-to-end.
Bugs E2E catches: misconfigured proxy, broken nav after deploy, auth flow regression, third-party script killing the page.
Cypress
Modern, JS-first E2E framework. Runs inside the browser.
describe('login', () => {
it('logs in and redirects', () => {
cy.visit('/login');
cy.get('[data-testid=email]').type('me@example.com');
cy.get('[data-testid=password]').type('hunter2');
cy.get('[data-testid=submit]').click();
cy.url().should('include', '/dashboard');
cy.contains('Welcome back').should('be.visible');
});
});Strengths:
- Synchronous-feeling API (auto-waits, auto-retries).
- Time-travel debugger — step through each command, see DOM snapshots.
- Network stubbing built in (
cy.intercept). - Excellent DX, fast iteration in dev.
- Test runner UI shows each command and assertion.
Weaknesses:
- Runs only in Chromium-family + Firefox + WebKit (now). No native Safari/IE.
- Single-tab, single-origin (improving but historically limited).
- Larger memory footprint per run.
Selenium
The classic. Drives any browser via WebDriver protocol.
const { Builder, By, until } = require('selenium-webdriver');
const driver = await new Builder().forBrowser('chrome').build();
await driver.get('https://app.example.com/login');
await driver.findElement(By.id('email')).sendKeys('me@example.com');
await driver.findElement(By.id('submit')).click();
await driver.wait(until.urlContains('/dashboard'), 5000);Strengths:
- Language-agnostic (JS, Python, Java, C#, Ruby).
- Drives every major browser (Chrome, Firefox, Safari, Edge, IE).
- Mature ecosystem (Selenium Grid for distributed runs).
- Industry standard for compliance / enterprise.
Weaknesses:
- Verbose. Lots of explicit waits.
- Flakier — race conditions are easy to write.
- Slower setup, slower per-test.
- DX feels dated compared to Cypress / Playwright.
Playwright (the modern alternative)
Worth mentioning since it's now the default choice for most new projects:
test('login', async ({ page }) => {
await page.goto('/login');
await page.getByTestId('email').fill('me@example.com');
await page.getByTestId('submit').click();
await expect(page).toHaveURL(/dashboard/);
});Combines Cypress-level DX with Selenium-level browser coverage. Multi-tab, multi-origin, parallel by default. Often the right pick today.
Cypress vs Selenium — when to pick which
| Need | Pick |
|---|---|
| Fast feedback, JS-first team | Cypress |
| Multi-language test suite | Selenium |
| Broad browser matrix | Selenium / Playwright |
| Strong dev debugging | Cypress |
| Multi-tab, multi-origin | Playwright |
| Greenfield project | Playwright |
| Legacy / regulated environments | Selenium |
Patterns for non-flaky E2E
E2E is famously flaky. Mitigations:
- Use data-testid attributes: not CSS selectors or text (which change with i18n / design).
- Auto-wait, don't sleep: Cypress and Playwright auto-retry; never
cy.wait(2000). - Stub external services: third-party APIs are uncontrollable; stub at network layer.
- Isolated test data: per-test seeded state; tests don't share fixtures.
- Reset between tests: clear cookies / localStorage / DB rows.
- Run against a deterministic build: not against a live dev server with hot reload.
- Retry once on failure in CI, then mark flaky for investigation.
How E2E fits the pyramid
/\
/E2E\ <- 5-20 critical paths
/------\
/ Integ. \ <- 50-200 module-level
/----------\
/ Unit \ <- 1000s, fast, isolated
/--------------\E2E is the tip. It validates the system holistically; everything else verifies it locally. A common mistake is inverting this — too many E2E, too few unit. Result: slow, flaky CI.
What to test in E2E
- Critical user paths: signup, login, checkout.
- Happy path for top features.
- Cross-page workflows that unit tests can't cover.
- Smoke tests post-deploy.
What NOT to test in E2E
- Edge cases of pure functions (unit).
- Component variants (component test).
- API contract (contract / unit test).
- Visual regression (visual diff tool).
CI integration
- Run on PR for affected paths, smoke set only.
- Run full suite on main / nightly.
- Parallelize across machines (Cypress Cloud, Selenium Grid, Playwright sharding).
- Surface failures with screenshots + video.
Mental model
E2E is the integration-level guardrail. It catches bugs no other layer can: bad deploys, broken third-party scripts, regressed user flows. Keep the suite small, focused on critical paths, fast, deterministic, and run on every PR. Cypress for JS-first teams with a single browser focus, Selenium for legacy / multi-language, Playwright for greenfield. The framework matters less than discipline: data-testids, auto-wait, stubbed externals, deterministic data.
Follow-up questions
- •How do you handle flaky E2E tests?
- •When should you choose Playwright over Cypress?
- •How do you stub network calls in Cypress?
- •How do you parallelize a Selenium suite?
Common mistakes
- •Using cy.wait(ms) instead of waiting for a condition.
- •Selecting by CSS class or text instead of data-testid.
- •Sharing test data between tests — order-dependent flakes.
- •Inverting the pyramid — 100 E2E, 10 unit.
- •Running against live external APIs — uncontrollable.
Performance considerations
- •Each E2E typically takes 2-30s. Suite of 100 tests = 5-30 min unparallelized. Sharding across 4-8 workers brings it under 5 min. Slow tests are the gateway drug to skipped suites.
Edge cases
- •Multi-tab flows (OAuth popups) — needs Playwright or Cypress workarounds.
- •File uploads — special handling per framework.
- •iframes — Cypress has historical limits; Playwright handles cleanly.
- •Mobile viewports — emulate vs real device.
Real-world examples
- •Cypress is widely used at startup / midsize companies.
- •Selenium remains dominant in enterprise / banking / healthcare.
- •Playwright has rapidly overtaken Cypress for new projects since 2023.
- •Most large orgs run a mix — Selenium for compliance, Playwright/Cypress for daily dev.