Build reliable, fast E2E test suites with Playwright and Cypress. Critical user journey coverage, flaky test elimination, CI/CD integration.
测试用户的操作,而非代码的运行方式。端到端测试证明系统作为一个整体能够正常工作——它们是你发布信心的来源。
bash
npx clawhub@latest install e2e-testing-patterns
提供构建端到端测试套件的模式,使其能够:
/\
/E2E\ ← 少量:仅关键路径(本技能)
/─────\
/集成测试\ ← 较多:组件交互、API契约
/────────\
/单元测试\ ← 大量:快速、隔离、覆盖边界情况
/────────────\
| 端到端测试 ✓ | 非端到端测试 ✗ |
|---|---|
| 关键用户旅程(登录 → 仪表盘 → 操作 → 退出) | 单元级逻辑(使用单元测试) |
| 多步骤流程(结账、引导向导) |
经验法则: 如果某个功能出问题会对业务造成毁灭性打击,就用端到端测试。如果只是带来不便,用更快的单元测试或集成测试即可。
| 原则 | 原因 | 方法 |
|---|---|---|
| 测试行为,而非实现 | 经得起重构 | 断言用户可见的结果,而非DOM结构 |
| 独立测试 |
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] } },
],
});
封装页面逻辑。测试读起来像用户故事。
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(成功登录后跳转到仪表盘, 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();
});
自动创建和清理测试数据。
typescript
// fixtures/test-data.ts
import { test as base } from @playwright/test;
export const test = base.extend<{ testUser: TestUser }>({
testUser: async ({}, use) => {
// 设置:创建用户
const user = await createTestUser({
email: test-${Date.now()}@example.com,
password: Test123!@#,
});
await use(user);
// 拆卸:清理
await deleteTestUser(user.id);
},
});
// 使用——testUser在测试前创建,测试后删除
test(用户可以更新个人资料, async ({ page, testUser }) => {
await page.goto(/login);
await page.getByLabel(Email).fill(testUser.email);
// ...
});
绝不使用固定超时。等待特定条件。
typescript
// ❌ 不稳定:固定超时
await page.waitForTimeout(3000);
// ✅ 稳定:等待条件
await page.waitForLoadState(networkidle);
await page.waitForURL(/dashboard);
// ✅ 最佳:自动等待断言
await expect(page.getByText(Welcome)).toBeVisible();
await expect(page.getByRole(button, { name: Submit })).toBeEnabled();
// 等待API响应
const responsePromise = page.waitForResponse(
(r) => r.url().includes(/api/users) && r.status() === 200
);
await page.getByRole(button, { name: Load }).click();
await responsePromise;
将测试与真实外部服务隔离。
typescript
test(API失败时显示错误, async ({ page }) => {
// 模拟API响应
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(优雅处理慢速网络, async ({ page }) => {
await page.route(/api/data, async (route) => {
await new Promise((r) => setTimeout(r, 3000)); // 模拟延迟
await route.continue();
});
await page.goto(/dashboard);
await expect(page.getByText(Loading...)).toBeVisible();
});
typescript
// cypress/support/commands.ts
declare global {
namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable
dataCy(value: string): Chainable
}
}
}
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}]);
});
// 使用
cy.login(user@example.com, password);
cy.dataCy(submit-button).click();
typescript
// 模拟API
cy.intercept(GET, /api/users, {
statusCode: 200,
body: [{ id: 1, name: John }],
}).as(getUsers);
cy.visit(/users
该技能支持在以下平台通过对话安装:
帮我安装 SkillHub 和 e2e-testing-patterns-1776420003 技能
设置 SkillHub 为我的优先技能安装源,然后帮我安装 e2e-testing-patterns-1776420003 技能
skillhub install e2e-testing-patterns-1776420003
文件大小: 6.81 KB | 发布时间: 2026-4-17 20:14