mcpField
E
e2e-testing

e2e-testing

github.com/hairyf测试与质量2698 次安装3 周前更新
代码仓库

关于 e2e-testing

E2E Testing 是面向开发者的技能,指导使用 Playwright 为完整用户流程编写浏览器端测试。内容涵盖页面对象、选择器、等待策略、测试数据、认证状态复用、抖动测试排查和 CI。

主要功能

  1. 01使用页面对象组织测试
  2. 02优先采用稳定的选择器
  3. 03使用显式等待而非休眠
  4. 04复用认证状态
  5. 05管理隔离的测试数据
  6. 06排查抖动测试并配置 CI

适用场景

  1. 01测试登录和多页面流程
  2. 02验证文件上传与下载
  3. 03运行跨浏览器回归检查
  4. 04创建部署冒烟测试

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/hairyf/skills --skill e2e-testing

安装后在 Claude Code 中输入/e2e-testing确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

SYNC.md
references/
e2e-test-template.ts
page-object-template.ts
playwright-config-example.ts
scripts/
run-e2e-with-report.sh

详细介绍

端到端测试

何时使用

在以下情况启用此技能:

  • 为完整的用户工作流(登录、CRUD 操作、多页面流程)编写端到端测试
  • 创建验证全栈的关键路径回归测试
  • 测试跨浏览器兼容性(Chromium、Firefox、WebKit)
  • 端到端验证身份认证流程
  • 测试文件上传/下载工作流
  • 为部署验证编写冒烟测试

不要在以下情况使用此技能:

  • React 组件单元测试(使用 react-testing-patterns)
  • Python 后端单元/集成测试(使用 pytest-patterns)
  • TDD 工作流强制执行(使用 tdd-workflow)
  • 无浏览器的 API 契约测试(使用 pytest-patterns 配合 httpx)

说明

测试结构
e2e/
├── playwright.config.ts         # Global Playwright configuration
├── fixtures/
│   ├── auth.fixture.ts          # Authentication state setup
│   └── test-data.fixture.ts     # Test data creation/cleanup
├── pages/
│   ├── base.page.ts             # Base page object with shared methods
│   ├── login.page.ts            # Login page object
│   ├── users.page.ts            # Users list page object
│   └── user-detail.page.ts     # User detail page object
├── tests/
│   ├── auth/
│   │   ├── login.spec.ts
│   │   └── logout.spec.ts
│   ├── users/
│   │   ├── create-user.spec.ts
│   │   ├── edit-user.spec.ts
│   │   └── list-users.spec.ts
│   └── smoke/
│       └── critical-paths.spec.ts
└── utils/
    ├── api-helpers.ts           # Direct API calls for test setup
    └── test-constants.ts        # Shared constants

命名约定:

  • 测试文件:<feature>.spec.ts
  • 页面对象:<page-name>.page.ts
  • Fixture:<concern>.fixture.ts
  • 测试名称:描述用户操作和预期结果的可读句子
页面对象模型

每个页面都有一个页面对象类,用于封装选择器和操作。测试从不直接与选择器交互。

基础页面对象:

// e2e/pages/base.page.ts
import { type Page, type Locator } from "@playwright/test";

export abstract class BasePage {
  constructor(protected readonly page: Page) {}

  /** Navigate to the page's URL. */
  abstract goto(): Promise<void>;

  /** Wait for the page to be fully loaded. */
  async waitForLoad(): Promise<void> {
    await this.page.waitForLoadState("networkidle");
  }

  /** Get a toast/notification message. */
  get toast(): Locator {
    return this.page.getByRole("alert");
  }

  /** Get the page heading. */
  get heading(): Locator {
    return this.page.getByRole("heading", { level: 1 });
  }
}

具体页面对象:

// e2e/pages/users.page.ts
import { type Page, type Locator } from "@playwright/test";
import { BasePage } from "./base.page";

export class UsersPage extends BasePage {
  // ─── Locators ─────────────────────────────────────────
  readonly createButton: Locator;
  readonly searchInput: Locator;
  readonly userTable: Locator;

  constructor(page: Page) {
    super(page);
    this.createButton = page.getByTestId("create-user-btn");
    this.searchInput = page.getByRole("searchbox", { name: /search users/i });
    this.userTable = page.getByRole("table");
  }

  // ─── Actions ──────────────────────────────────────────
  async goto(): Promise<void> {
    await this.page.goto("/users");
    await this.waitForLoad();
  }

  async searchFor(query: string): Promise<void> {
    await this.searchInput.fill(query);
    // Wait for search results to update (debounced)
    await this.page.waitForResponse("**/api/v1/users?*");
  }

  async clickCreateUser(): Promise<void> {
    await this.createButton.click();
  }

  async getUserRow(email: string): Promise<Locator> {
    return this.userTable.getByRole("row").filter({ hasText: email });
  }

  async getUserCount(): Promise<number> {
    // Subtract 1 for header row
    return (await this.userTable.getByRole("row").count()) - 1;
  }
}

页面对象规则:

  • 每个页面或主要 UI 区域对应一个页面对象
  • 定位器是 public readonly 属性
  • 操作是 async 方法
  • 页面对象中从不包含断言——由测试进行断言
  • 页面对象在操作后内部处理等待
选择器策略

优先级顺序(从高到低):

优先级选择器示例何时使用
1data-testidgetByTestId("submit-btn")交互元素、动态内容
2RolegetByRole("button", { name: /save/i })按钮、链接、标题、输入框
3LabelgetByLabel("Email")带标签的表单输入
4PlaceholdergetByPlaceholder("Search...")搜索输入框
5TextgetByText("Welcome back")静态文本内容

绝不使用:

  • CSS 选择器(.class-name、#id)——脆弱,样式变更即失效
  • XPath(//div[@class="foo"])——不可读,极其脆弱
  • DOM 结构选择器(div > span:nth-child(2))——布局变更即失效

添加 data-testid 属性:

// In React components -- add data-testid to interactive elements
<button data-testid="create-user-btn" onClick={handleCreate}>
  Create User
</button>

// Convention: kebab-case, descriptive
// Pattern: <action>-<entity>-<element-type>
// Examples: create-user-btn, user-email-input, delete-confirm-dialog
等待策略

绝不使用硬编码等待:

// BAD: Hardcoded wait -- flaky, slow
await page.waitForTimeout(3000);

// BAD: Sleep
await new Promise((resolve) => setTimeout(resolve, 2000));

使用显式等待条件:

// GOOD: Wait for a specific element to appear
await page.getByRole("heading", { name: "Dashboard" }).waitFor();

// GOOD: Wait for navigation
await page.waitForURL("/dashboard");

// GOOD: Wait for API response
await page.waitForResponse(
  (response) =>
    response.url().includes("/api/v1/users") && response.status() === 200,
);

// GOOD: Wait for network to settle
await page.waitForLoadState("networkidle");

// GOOD: Wait for element state
await page.getByTestId("submit-btn").waitFor({ state: "visible" });
await page.getByTestId("loading-spinner").waitFor({ state: "hidden" });

自动等待: Playwright 会在点击、填写等操作前自动等待元素可交互。仅在断言或复杂状态转换时才需要显式等待。

认证状态复用

避免在每个测试前都登录。保存认证状态并复用。

一次性设置认证状态:

// e2e/fixtures/auth.fixture.ts
import { test as base } from "@playwright/test";
import path from "path";

const AUTH_STATE_PATH = path.resolve("e2e/.auth/user.json");

export const setup = base.extend({});

setup("authenticate", async ({ page }) => {
  // Perform real login
  await page.goto("/login");
  await page.getByLabel("Email").fill("[email protected]");
  await page.getByLabel("Password").fill("TestPassword123!");
  await page.getByRole("button", { name: /sign in/i }).click();

  // Wait for auth to complete
  await page.waitForURL("/dashboard");

  // Save signed-in state
  await page.context().storageState({ path: AUTH_STATE_PATH });
});

在测试中复用:

// playwright.config.ts
export default defineConfig({
  projects: [
    // Setup project runs first and saves auth state
    { name: "setup", testDir: "./e2e/fixtures", testMatch: "auth.fixture.ts" },
    {
      name: "chromium",
      use: {
        storageState: "e2e/.auth/user.json",  // Reuse auth state
      },
      dependencies: ["setup"],
    },
  ],
});
测试数据管理

原则:

  • 测试创建自己的数据(绝不依赖预先存在的数据)
  • 测试自行清理(或使用 API 重置)
  • 使用 API 调用进行设置,而非 UI 交互(更快、更可靠)

用于测试数据的 API 辅助函数:

// e2e/utils/api-helpers.ts
import { type APIRequestContext } from "@playwright/test";

export class TestDataAPI {
  constructor(private request: APIRequestContext) {}

  async createUser(data: { email: string; displayName: string }) {
    const response = await this.request.post("/api/v1/users", { data });
    return response.json();
  }

  async deleteUser(userId: number) {
    await this.request.delete(`/api/v1/users/${userId}`);
  }

  async createOrder(userId: number, items: Array<Record<string, unknown>>) {
    const response = await this.request.post("/api/v1/orders", {
      data: { user_id: userId, items },
    });
    return response.json();
  }
}

在测试中的用法:

test("edit user name", async ({ page, request }) => {
  const api = new TestDataAPI(request);

  // Setup: create user via API (fast)
  const user = await api.createUser({
    email: "[email protected]",
    displayName: "Before Edit",
  });

  try {
    // Test: edit via UI
    const usersPage = new UsersPage(page);
    await usersPage.goto();
    // ... perform edit via UI ...
  } finally {
    // Cleanup: remove test data
    await api.deleteUser(user.id);
  }
});
调试不稳定测试

1. 使用 trace viewer 查看失败:

// playwright.config.ts
use: {
  trace: "on-first-retry",  // Capture trace only on retry
}

查看 trace:npx playwright show-trace trace.zip

2. 以有头模式运行以进行调试:

npx playwright test --headed --debug tests/users/create-user.spec.ts

3. 不稳定测试的常见原因:

原因修复方法
硬编码等待使用显式等待条件
共享测试数据每个测试创建自己的数据
动画干扰在配置中设置 animations: "disabled"
竞态条件在断言前等待 API 响应
依赖视口的行为在配置中设置明确的视口
测试之间的会话泄漏正确使用 storageState,清除 cookie

4. 重试策略:

// playwright.config.ts
export default defineConfig({
  retries: process.env.CI ? 2 : 0,  // Retry in CI only
});
CI 配置
# .github/workflows/e2e.yml
name: E2E Tests
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright browsers
        run: npx playwright install --with-deps chromium

      - name: Start application
        run: |
          docker compose up -d
          npx wait-on http://localhost:3000 --timeout 60000

      - name: Run E2E tests
        run: npx playwright test

      - name: Upload test report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 14

      - name: Upload traces on failure
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: test-traces
          path: test-results/

使用 scripts/run-e2e-with-report.sh 在本地运行 Playwright 并输出 HTML 报告。

示例

有关带注释的页面对象类,请参阅 references/page-object-template.ts。 有关带注释的 E2E 测试,请参阅 references/e2e-test-template.ts。 有关生产环境 Playwright 配置,请参阅 references/playwright-config-example.ts。

评论

支持 Markdown