E2E Testing Patterns
Build reliable, fast, and maintainable end-to-end test suites that provide confidence to ship code quickly and catch regressions before users do.
When to Use This Skill
-
Implementing end-to-end test automation
-
Debugging flaky or unreliable tests
-
Testing critical user workflows
-
Setting up CI/CD test pipelines
-
Testing across multiple browsers
-
Validating accessibility requirements
-
Testing responsive designs
-
Establishing E2E testing standards
Core Concepts
- E2E Testing Fundamentals
What to Test with E2E:
-
Critical user journeys (login, checkout, signup)
-
Complex interactions (drag-and-drop, multi-step forms)
-
Cross-browser compatibility
-
Real API integration
-
Authentication flows
What NOT to Test with E2E:
-
Unit-level logic (use unit tests)
-
API contracts (use integration tests)
-
Edge cases (too slow)
-
Internal implementation details
- Test Philosophy
The Testing Pyramid:
/\
/E2E\ ← Few, focused on critical paths
/─────\
/Integr\ ← More, test component interactions
/────────\
/Unit Tests\ ← Many, fast, isolated /────────────\
Best Practices:
-
Test user behavior, not implementation
-
Keep tests independent
-
Make tests deterministic
-
Optimize for speed
-
Use data-testid, not CSS selectors
Framework Patterns
For detailed Playwright and Cypress code examples including:
-
Page Object Model
-
Fixtures for test data
-
Waiting strategies
-
Network mocking and interception
-
Custom commands
-
Visual regression testing
-
Parallel testing with sharding
-
Accessibility testing
👉 examples/test-patterns.md
Best Practices
-
Use Data Attributes: data-testid or data-cy for stable selectors
-
Avoid Brittle Selectors: Don't rely on CSS classes or DOM structure
-
Test User Behavior: Click, type, see - not implementation details
-
Keep Tests Independent: Each test should run in isolation
-
Clean Up Test Data: Create and destroy test data in each test
-
Use Page Objects: Encapsulate page logic
-
Meaningful Assertions: Check actual user-visible behavior
-
Optimize for Speed: Mock when possible, parallel execution
// ❌ Bad selectors cy.get(".btn.btn-primary.submit-button").click(); cy.get("div > form > div:nth-child(2) > input").type("text");
// ✅ Good selectors cy.getByRole("button", { name: "Submit" }).click(); cy.getByLabel("Email address").type("user@example.com"); cy.get('[data-testid="email-input"]').type("user@example.com");
Common Pitfalls
-
Flaky Tests: Use proper waits, not fixed timeouts
-
Slow Tests: Mock external APIs, use parallel execution
-
Over-Testing: Don't test every edge case with E2E
-
Coupled Tests: Tests should not depend on each other
-
Poor Selectors: Avoid CSS classes and nth-child
-
No Cleanup: Clean up test data after each test
-
Testing Implementation: Test user behavior, not internals
Debugging Failing Tests
// Playwright debugging // 1. Run in headed mode npx playwright test --headed
// 2. Run in debug mode npx playwright test --debug
// 3. Use trace viewer await page.screenshot({ path: 'screenshot.png' }); await page.video()?.saveAs('video.webm');
// 4. Add test.step for better reporting test('checkout flow', async ({ page }) => { await test.step('Add item to cart', async () => { await page.goto('/products'); await page.getByRole('button', { name: 'Add to Cart' }).click(); });
await test.step('Proceed to checkout', async () => {
await page.goto('/cart');
await page.getByRole('button', { name: 'Checkout' }).click();
});
});
// 5. Inspect page state await page.pause(); // Pauses execution, opens inspector
Resources
-
references/playwright-best-practices.md: Playwright-specific patterns
-
references/cypress-best-practices.md: Cypress-specific patterns
-
references/flaky-test-debugging.md: Debugging unreliable tests
-
assets/e2e-testing-checklist.md: What to test with E2E
-
assets/selector-strategies.md: Finding reliable selectors
-
scripts/test-analyzer.ts: Analyze test flakiness and duration