mcpField
E
e2e-testing

e2e-testing

github.com/hieutrtr测试与质量81.6k 次安装38 周前更新
代码仓库

关于 e2e-testing

e2e-testing 是用于设计 Playwright 端到端测试的技能,覆盖完整用户工作流和关键路径。它提供页面对象、选择器、测试数据、认证复用、调试及 CI 方面的指导。

主要功能

  1. 01Playwright E2E 测试结构
  2. 02页面对象模型指导
  3. 03可靠的选择器策略
  4. 04显式等待与认证复用
  5. 05测试数据创建与清理
  6. 06不稳定测试调试及 CI

适用场景

  1. 01测试登录和 CRUD 工作流
  2. 02检查跨浏览器行为
  3. 03验证文件上传与下载流程
  4. 04运行部署冒烟测试

安装

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

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

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

包含的文件

除 SKILL.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