Files
gh-tachyon-beep-skillpacks-…/skills/e2e-testing-strategies/SKILL.md
2025-11-30 08:59:43 +08:00

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.