Tags: e2e, testing, playwright, cypress, selenium, qa
Level: intermediate
TL;DR: End-to-End (E2E) testing validates the entire software application from start to finish, ensuring integrated components, databases, and network layers function together under real-world scenarios.
Validates the complete system flow and user journey.
Simulates real user interactions with the application interface.
Sits at the top of the testing pyramid (high confidence, high cost).
Slower execution compared to unit or integration tests.
Susceptible to flakiness due to network latency and UI rendering delays.
Tests multiple layers simultaneously (UI, API, database, third-party services).
Requires a dedicated, reproducible test environment to avoid state pollution.
Q: Why do we need E2E tests if we have 100% unit test coverage?R: Unit tests verify components in isolation. I use E2E tests to catch integration bugs, such as misconfigured API endpoints, database locks, or UI routing issues that only appear when the whole system runs together.
Q: How do you handle flaky E2E tests?R: I replace hardcoded timeouts with dynamic waits for element states (e.g., waiting for an element to be visible). I also ensure test data is isolated and properly seeded or mocked before each run.
Q: What is the difference between Cypress and Playwright?R: Cypress runs inside the browser with a single-process architecture, making it great for frontend-heavy apps. I prefer Playwright for cross-browser support (Webkit, Chromium, Firefox) and its native handling of multiple tabs and iframes.
Using fixed sleep intervals instead of waiting for explicit DOM mutations.
Attempting to test exhaustive business logic edge cases (better suited for unit tests).
Using brittle CSS selectors (like deeply nested tags) instead of stable data attributes (data-testid).
Failing to clean up or reset database state between test runs.
Running E2E tests against production environments instead of staging.
// Basic Playwright test example
import { test, expect } from '@playwright/test';
test('user can log in and view dashboard', async ({ page }) => {
await page.goto('https://staging-app.local/login');
// Use robust data-testid selectors
await page.getByTestId('email-input').fill('testuser@example.com');
await page.getByTestId('password-input').fill('SecurePass123!');
await page.getByTestId('submit-button').click();
// Dynamically wait for the URL change and element visibility
await expect(page).toHaveURL(/.*dashboard/);
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
});
Bash
# Run Playwright tests with UI mode for debugging
npx playwright test --ui
# Run Playwright tests in headed mode to watch execution
npx playwright test --headed