291 lines
9.0 KiB
Markdown
291 lines
9.0 KiB
Markdown
---
|
|
name: e2e-testing-strategies
|
|
description: Use when designing E2E test architecture, choosing between Cypress/Playwright/Selenium, prioritizing which flows to test, fixing flaky E2E tests, or debugging slow E2E test suites - provides production-tested patterns and anti-patterns
|
|
---
|
|
|
|
# E2E Testing Strategies
|
|
|
|
## Overview
|
|
|
|
**Core principle:** E2E tests are expensive. Use them sparingly for critical multi-system flows. Everything else belongs lower in the test pyramid.
|
|
|
|
**Test pyramid target:** 5-10% E2E, 20-25% integration, 65-75% unit
|
|
|
|
**Scope:** This skill focuses on web application E2E testing (browser-based). For mobile app testing (iOS/Android), decision tree points to Appium, but patterns/anti-patterns here are web-specific. Mobile testing requires different strategies for device capabilities, native selectors, and app lifecycle.
|
|
|
|
## Framework Selection Decision Tree
|
|
|
|
Choose framework based on constraints:
|
|
|
|
| Your Constraint | Choose | Why |
|
|
|----------------|--------|-----|
|
|
| Need cross-browser (Chrome/Firefox/Safari) | **Playwright** | Native multi-browser, auto-wait, trace viewer |
|
|
| Team unfamiliar with testing | **Cypress** | Simpler API, better DX, larger community |
|
|
| Enterprise/W3C standard requirement | **WebdriverIO** | Full W3C WebDriver protocol |
|
|
| Headless Chrome only, fine-grained control | **Puppeteer** | Lower-level, faster for Chrome-only |
|
|
| Testing Electron apps | **Spectron** or **Playwright** | Native Electron support |
|
|
| Mobile apps (iOS/Android) | **Appium** | Mobile-specific protocol (Note: rest of this skill is web-focused) |
|
|
|
|
**For most web apps:** Playwright (modern, reliable) or Cypress (simpler DX)
|
|
|
|
## Flow Prioritization Matrix
|
|
|
|
When you have 50 flows but can only test 10 E2E:
|
|
|
|
| Score | Criteria | Weight |
|
|
|-------|----------|--------|
|
|
| +3 | Revenue impact (checkout, payment, subscription) | High |
|
|
| +3 | Multi-system integration (API + DB + email + payment) | High |
|
|
| +2 | Historical production failures (has broken before) | Medium |
|
|
| +2 | Complex state management (auth, sessions, caching) | Medium |
|
|
| +1 | User entry point (login, signup, search) | Medium |
|
|
| +1 | Regulatory/compliance requirement | Medium |
|
|
| -2 | Can be tested at integration level | Penalty |
|
|
| -3 | Mostly UI interaction, no backend | Penalty |
|
|
|
|
**Score flows 0-10, test top 10.** Everything else → integration/unit tests.
|
|
|
|
**Example:**
|
|
- "User checkout flow" = +3 revenue +3 multi-system +2 historical +2 state = **10** → E2E
|
|
- "User changes email preference" = +1 entry -2 integration level = **-1** → Integration test
|
|
|
|
## Anti-Patterns Catalog
|
|
|
|
### ❌ Pyramid Inversion
|
|
**Symptom:** 200 E2E tests, 50 integration tests, 100 unit tests
|
|
|
|
**Why bad:** E2E tests are slow (30min CI), brittle (UI changes break tests), hard to debug
|
|
|
|
**Fix:** Invert back - move 150 E2E tests down to integration/unit
|
|
|
|
---
|
|
|
|
### ❌ Testing Through the UI
|
|
**Symptom:** E2E test creates 10 users through signup form to test one admin feature
|
|
|
|
**Why bad:** Slow, couples unrelated features
|
|
|
|
**Fix:** Seed data via API/database, test only the admin feature flow
|
|
|
|
---
|
|
|
|
### ❌ Arbitrary Timeouts
|
|
**Symptom:** `wait(5000)` sprinkled throughout tests
|
|
|
|
**Why bad:** Flaky - sometimes too short, sometimes wastes time
|
|
|
|
**Fix:** Explicit waits for conditions
|
|
```javascript
|
|
// ❌ Bad
|
|
await page.click('button');
|
|
await page.waitForTimeout(5000);
|
|
|
|
// ✅ Good
|
|
await page.click('button');
|
|
await page.waitForSelector('.success-message');
|
|
```
|
|
|
|
---
|
|
|
|
### ❌ God Page Objects
|
|
**Symptom:** Single `PageObject` class with 50 methods for entire app
|
|
|
|
**Why bad:** Tight coupling, hard to maintain, unclear responsibilities
|
|
|
|
**Fix:** One page object per logical page/component
|
|
```javascript
|
|
// ❌ Bad: God object
|
|
class AppPage {
|
|
async login() {}
|
|
async createPost() {}
|
|
async deleteUser() {}
|
|
async exportReport() {}
|
|
// ... 50 more methods
|
|
}
|
|
|
|
// ✅ Good: Focused page objects
|
|
class AuthPage {
|
|
async login() {}
|
|
async logout() {}
|
|
}
|
|
|
|
class PostsPage {
|
|
async create() {}
|
|
async delete() {}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
###❌ Brittle Selectors
|
|
**Symptom:** `page.click('.btn-primary.mt-4.px-3')`
|
|
|
|
**Why bad:** Breaks when CSS changes
|
|
|
|
**Fix:** Use `data-testid` attributes
|
|
```javascript
|
|
// ❌ Bad
|
|
await page.click('.submit-button.btn.btn-primary');
|
|
|
|
// ✅ Good
|
|
await page.click('[data-testid="submit"]');
|
|
```
|
|
|
|
---
|
|
|
|
### ❌ Test Interdependence
|
|
**Symptom:** Test 5 fails if Test 3 doesn't run first
|
|
|
|
**Why bad:** Can't run tests in parallel, hard to debug
|
|
|
|
**Fix:** Each test sets up own state
|
|
```javascript
|
|
// ❌ Bad
|
|
test('create user', async () => {
|
|
// creates user "test@example.com"
|
|
});
|
|
|
|
test('login user', async () => {
|
|
// assumes user from previous test exists
|
|
});
|
|
|
|
// ✅ Good
|
|
test('login user', async ({ page }) => {
|
|
await createUserViaAPI('test@example.com'); // independent setup
|
|
await page.goto('/login');
|
|
// test login flow
|
|
});
|
|
```
|
|
|
|
## Flakiness Patterns Catalog
|
|
|
|
Common flake sources and fixes:
|
|
|
|
| Pattern | Symptom | Fix |
|
|
|---------|---------|-----|
|
|
| **Network Race** | "Element not found" intermittently | `await page.waitForLoadState('networkidle')` |
|
|
| **Animation Race** | "Element not clickable" | `await page.waitForSelector('.element', { state: 'visible' })` or disable animations |
|
|
| **Async State** | "Expected 'success' but got ''" | Wait for specific state, not timeout |
|
|
| **Test Data Pollution** | Test passes alone, fails in suite | Isolate data per test (unique IDs, cleanup) |
|
|
| **Browser Caching** | Different results first vs second run | Clear cache/cookies between tests |
|
|
| **Date/Time Sensitivity** | Test fails at midnight, passes during day | Mock system time in tests |
|
|
| **External Service** | Third-party API occasionally down | Mock external dependencies |
|
|
|
|
**Rule:** If test fails <5% of time, it's flaky. Fix it before adding more tests.
|
|
|
|
## Page Object Anti-Patterns
|
|
|
|
### ❌ Business Logic in Page Objects
|
|
```javascript
|
|
// ❌ Bad
|
|
class CheckoutPage {
|
|
async calculateTotal(items) {
|
|
return items.reduce((sum, item) => sum + item.price, 0); // business logic
|
|
}
|
|
}
|
|
|
|
// ✅ Good
|
|
class CheckoutPage {
|
|
async getTotal() {
|
|
return await page.textContent('[data-testid="total"]'); // UI interaction only
|
|
}
|
|
}
|
|
```
|
|
|
|
### ❌ Assertions in Page Objects
|
|
```javascript
|
|
// ❌ Bad
|
|
class LoginPage {
|
|
async login(email, password) {
|
|
await this.page.fill('[data-testid="email"]', email);
|
|
await this.page.fill('[data-testid="password"]', password);
|
|
await this.page.click('[data-testid="submit"]');
|
|
expect(this.page.url()).toContain('/dashboard'); // assertion
|
|
}
|
|
}
|
|
|
|
// ✅ Good
|
|
class LoginPage {
|
|
async login(email, password) {
|
|
await this.page.fill('[data-testid="email"]', email);
|
|
await this.page.fill('[data-testid="password"]', password);
|
|
await this.page.click('[data-testid="submit"]');
|
|
}
|
|
|
|
async isOnDashboard() {
|
|
return this.page.url().includes('/dashboard');
|
|
}
|
|
}
|
|
|
|
// Test file handles assertions
|
|
test('login', async () => {
|
|
await loginPage.login('user@test.com', 'password');
|
|
expect(await loginPage.isOnDashboard()).toBe(true);
|
|
});
|
|
```
|
|
|
|
## Quick Reference
|
|
|
|
### When to Use E2E vs Integration vs Unit
|
|
|
|
| Scenario | Test Level | Reasoning |
|
|
|----------|-----------|-----------|
|
|
| Form validation logic | Unit | Pure function, no UI needed |
|
|
| API error handling | Integration | Test API contract, no browser |
|
|
| Multi-step checkout | E2E | Crosses systems, critical revenue |
|
|
| Button hover states | Visual regression | Not functional behavior |
|
|
| Login → dashboard redirect | E2E | Auth critical, multi-system |
|
|
| Database query performance | Integration | No UI, just DB |
|
|
| User can filter search results | E2E (1 test) + Integration (variations) | 1 E2E for happy path, rest integration |
|
|
|
|
### Test Data Strategies
|
|
|
|
| Approach | When to Use | Pros | Cons |
|
|
|----------|-------------|------|------|
|
|
| **API Seeding** | Most tests | Fast, consistent | Requires API access |
|
|
| **Database Seeding** | Integration tests | Complete control | Slow, requires DB access |
|
|
| **UI Creation** | Testing creation flow itself | Tests real user path | Slow, couples tests |
|
|
| **Mocking** | External services | Fast, reliable | Misses real integration issues |
|
|
| **Fixtures** | Consistent test data | Reusable, version-controlled | Stale if schema changes |
|
|
|
|
## Common Mistakes
|
|
|
|
### ❌ Running Full Suite on Every Commit
|
|
**Symptom:** 30-minute CI blocking every PR
|
|
|
|
**Fix:** Smoke tests (5-10 critical flows) on PR, full suite on merge/nightly
|
|
|
|
---
|
|
|
|
### ❌ Not Capturing Failure Artifacts
|
|
**Symptom:** "Test failed in CI but I can't reproduce"
|
|
|
|
**Fix:** Save video + trace on failure
|
|
```javascript
|
|
// playwright.config.js
|
|
use: {
|
|
video: 'retain-on-failure',
|
|
trace: 'retain-on-failure',
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### ❌ Testing Implementation Details
|
|
**Symptom:** Tests assert internal component state
|
|
|
|
**Fix:** Test user-visible behavior only
|
|
|
|
---
|
|
|
|
### ❌ One Assert Per Test
|
|
**Symptom:** 50 E2E tests all navigate to same page, test one thing
|
|
|
|
**Fix:** Group related assertions in one flow test (but keep focused)
|
|
|
|
## Bottom Line
|
|
|
|
**E2E tests verify critical multi-system flows work for real users.**
|
|
|
|
If you can test it faster/more reliably at a lower level, do that instead.
|