E2E Testing Patterns
由微软开源的 Playwright 与 Cypress E2E 测试最佳实践指南,提供消除不稳定测试、CI/CD 集成及关键用户路径覆盖的企业级模式。
Install 安装
curl -sSL https://updating.cc/skills/cocoloop/qa-e2e-testing-patterns/SKILL.md -o ~/.agents/skills/cocoloop__qa-e2e-testing-patterns/SKILL.mdSKILL.md Preview 技能内容预览
---
name: e2e-testing-patterns
model: standard
category: testing
description: Build reliable, fast E2E test suites with Playwright and Cypress. Critical user journey coverage, flaky test elimination, CI/CD integration.
version: 1.0
keywords: [e2e, end-to-end, playwright, cypress, browser testing, integration tests, test automation, flaky tests, visual regression]
---
# E2E Testing Patterns
> Test what users do, not how code works. E2E tests prove the system works as a whole — they're your confidence to ship.
## Installation
### OpenClaw / Moltbot / Clawbot
```bash
npx clawhub@latest install e2e-testing-patterns
```
---
## WHAT This Skill Does
Provides patterns for building end-to-end test suites that:
- Catch regressions before users do
- Run fast enough for CI/CD
- Remain stable (no flaky failures)
- Cover critical user journeys without over-testing
## WHEN To Use
- **Implementing E2E test automation** for a web application
- **Debugging flaky tests** that fail intermittently
- **Setting up CI/CD test pipelines** with browser tests
- **Testing critical user workflows** (auth, checkout, signup)
- **Choosing what to test with E2E** vs unit/integration tests
---
## Test Pyramid — Know Your Layer
```
/\
/E2E\ ← FEW: Critical paths only (this skill)
/─────\
/Integr\ ← MORE: Component interactions, API contracts
/────────\
/Unit Tests\ ← MANY: Fast, isolated, cover edge cases
/────────────\
```
### What E2E Tests Are For
| E2E Tests ✓ | NOT E2E Tests ✗ |
|-------------|-----------------|
| Critical user journeys (login → dashboard → action → logout) | Unit-level logic (use unit tests) |
| Multi-step flows (checkout, onboarding wizard) | API contracts (use integration tests) |
| Cross-browser compatibility | Edge cases (too slow, use unit tests) |
| Real API integration | Internal implementation details |
| Authentication flows | Component visual states (use Storybook) |
**Rule of thumb:** If it would devastate your business to break, E2E test it. If it's just inconvenient, test it faster with unit/integration tests.
---
## Core Principles
| Principle | Why | How |
|-----------|-----|-----|
| **Test behavior, not implementation** | Survives refactors | Assert on user-visible outcomes, not DOM structure |
| **Independent tests** | Parallelizable, debuggable | Each test creates its own data, cleans up after |
| **Deterministic waits** | No flakiness | Wait for conditions, not fixed timeouts |
| **Stable selectors** | Survives UI changes | Use `data-testid`, roles, labels — never CSS classes |
| **Fast feedback** | Developers run them | Mock external services, parallelize, shard |
---
## Playwright Patterns
### Configuration
```typescript
// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
timeout: 30000,
expect: { timeout: 5000 },
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [["html"], ["junit", { outputFile: "results.xml" }]],
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry",
screenshot: "only-on-failure",
video: "retain-on-failure",
},
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
{ name: "mobile", use: { ...devices["iPhone 13"] } },
],
});
```
### Pattern: Page Object Model
Encapsulate page logic. Tests read like user stories.
```typescript
// pages/LoginPage.ts
import { Page, Locator } from "@playwright/test";
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel("Email");
this.passwordInput = page.getByLabel("Password");
this.loginButton = page.getByRole("button", { name: "Login" });
this.errorMessage = page.getByRole("alert");
}
async goto() {
await this.page.goto("/login");
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
}
// tests/login.spec.ts
import { test, expect } from "@playwright/test";
import { LoginPage } from "../pages/LoginPage";
test("successful login redirects to dashboard", async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login("user@example.com", "password123");
await expect(page).toHaveURL("/dashboard");
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
});
```
### Pattern: Fixtures for Test Data
Create and clean up test data automatically.
```typescript
// fixtures/test-data.ts
import { test as base } from "@playwright/test";
export const test = base.extend<{ testUser: TestUser }>({
testUser: async ({}, use) => {
// Setup: Create user
const user = await createTestUser({
email: `test-${Date.now()}@example.com`,
password: "Test123!@#",
});
await use(user);
// Teardown: Clean up
await deleteTestUser(user.id);
},
});
// Usage — testUser is created before, deleted after
test("user can update profile", async ({ page, testUser }) => {
await page.goto("/login");
await page.getByLabel("Email").fill(testUser.email);
// ...
});
```
### Pattern: Smart Waiting
Never use fixed timeouts. Wait for specific conditions.
```typescript
// ❌ FLAKY: Fixed timeout
await page.waitForTimeout(3000);
// ✅ STABLE: Wait for conditions
await page.waitForLoadState("networkidle");
await page.waitForURL("/dashboard");
// ✅ BEST: Auto-waiting assertions
await expect(page.getByText("Welcome")).toBeVisible();
await expect(page.getByRole("button", { name: "Submit" })).toBeEnabled();
// Wait for API response
const responsePromise = page.waitForResponse(
(r) => r.url().includes("/api/users") && r.status() === 200
);
await page.getByRole("button", { name: "Load" }).click();
await responsePromise;
```
### Pattern: Network Mocking
Isolate tests from real external services.
```typescript
test("shows error when API fails", async ({ page }) => {
// Mock the API response
await page.route("**/api/users", (route) => {
route.fulfill({
status: 500,
body: JSON.stringify({ error: "Server Error" }),
});
});
await page.goto("/users");
await expect(page.getByText("Failed to load users")).toBeVisible();
});
test("handles slow network gracefully", async ({ page }) => {
await page.route("**/api/data", async (route) => {
await new Promise((r) => setTimeout(r, 3000)); // Simulate delay
await route.continue();
});
await page.goto("/dashboard");
await expect(page.getByText("Loading...")).toBeVisible();
});
```
---
## Cypress Patterns
### Custom Commands
```typescript
// cypress/support/commands.ts
declare global {
namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable<void>;
dataCy(value: string): Chainable<JQuery<HTMLElement>>;
}
}
}
Cypress.Commands.add("login", (email, password) => {
cy.visit("/login");
cy.get('[data-testid="email"]').type(email);
cy.get('[data-testid="password"]').type(password);
cy.get('[data-testid="login-button"]').click();
cy.url().should("include", "/dashboard");
});
Cypress.Commands.add("dataCy", (value) => {
return cy.get(`[data-cy="${value}"]`);
});
// Usage
cy.login("user@example.com", "password");
cy.dataCy("submit-button").click();
```
### Network Intercepts
```typescript
// Mock API
cy.intercept("GET", "/api/users", {
statusCode: 200,
body: [{ id: 1, name: "John" }],
}).as("getUsers");
cy.visit("/users");
cy.wait("@getUsers");
cy.get('[data-testid="u