网淘吧来吧,欢迎您!

返回首页 微信
微信
手机版
手机版

E2E Testing Patterns

2026-03-28 新闻来源:网淘吧 围观:17
电脑广告
手机广告

端到端测试模式

测试用户行为,而非代码实现。端到端测试验证系统整体运行——它们是您交付产品的信心保障。

安装说明

OpenClaw / Moltbot / Clawbot

npx clawhub@latest install e2e-testing-patterns

技能功能说明

提供构建端到端测试套件的模式,可实现:

  • 在用户发现问题前捕获回归缺陷
  • 满足CI/CD流程的速度要求
  • 保持测试稳定性(避免偶发性失败)
  • 覆盖核心用户流程,避免过度测试

适用场景

  • 为Web应用程序实施端到端测试自动化
  • 调试间歇性失败的不稳定性测试
  • 搭建包含浏览器测试的CI/CD测试流水线
  • 测试核心用户工作流(身份验证、支付结算、注册流程)
  • 如何选择端到端测试与单元/集成测试的适用场景

测试金字塔——了解你的层级

        /\
       /E2E\         ← FEW: Critical paths only (this skill)
      /─────\
     /Integr\        ← MORE: Component interactions, API contracts
    /────────\
   /Unit Tests\      ← MANY: Fast, isolated, cover edge cases
  /────────────\

端到端测试的用途

适合进行端到端测试的内容 ✓不适合进行端到端测试的内容 ✗
关键用户旅程(登录 → 仪表板 → 操作 → 登出)单元级别的逻辑(应使用单元测试)
多步骤流程(结账、新用户引导向导)API 契约(应使用集成测试)
跨浏览器兼容性边界情况(速度太慢,应使用单元测试)
真实的 API 集成内部实现细节
身份验证流程组件视觉状态(应使用 Storybook)

经验法则:如果某项功能出错会严重损害你的业务,就进行端到端测试。如果只是带来不便,就用单元/集成测试更快地测试它。


核心原则

原则原因方法
测试行为,而非实现在重构中保持稳定基于用户可见的结果进行断言,而非DOM结构
独立测试可并行化,易于调试每个测试创建自己的数据,并在完成后清理
确定性等待无随机性失败等待条件满足,而非固定超时
稳定的选择器在UI变更中保持稳定使用data-testid、角色、标签——永远不要使用CSS类
快速反馈由开发者运行模拟外部服务、并行化、分片

Playwright模式

配置

// 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"] } },
  ],
});

模式:页面对象模型

封装页面逻辑。测试读起来像用户故事。

// 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();
});

模式:测试数据夹具

自动创建和清理测试数据。

// 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);
  // ...
});

模式:智能等待

切勿使用固定的超时时间。等待特定条件。

// ❌ 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;

模式:网络模拟

将测试与真实的外部服务隔离。

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 模式

自定义命令

// 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();

网络拦截

// 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="user-list"]').children().should("have.length", 1);

选择器策略

优先级选择器类型示例原因
1角色 + 名称getByRole("button", { name: "提交" })可访问,面向用户
2标签getByLabel("电子邮件地址")可访问,语义化
3data-testidgetByTestId("checkout-form")稳定,明确用于测试
4文本内容getByText("欢迎回来")面向用户
CSS 类.btn-primary样式变更时失效
DOM 结构div > form > input:nth-child(2)任何结构重组时失效
// ❌ BAD: Brittle selectors
cy.get(".btn.btn-primary.submit-button").click();
cy.get("div > form > div:nth-child(2) > input").type("text");

// ✅ GOOD: Stable selectors
page.getByRole("button", { name: "Submit" }).click();
page.getByLabel("Email address").fill("user@example.com");
page.getByTestId("email-input").fill("user@example.com");

视觉回归测试

// Playwright visual comparisons
test("homepage looks correct", async ({ page }) => {
  await page.goto("/");
  await expect(page).toHaveScreenshot("homepage.png", {
    fullPage: true,
    maxDiffPixels: 100,
  });
});

test("button states", async ({ page }) => {
  const button = page.getByRole("button", { name: "Submit" });

  await expect(button).toHaveScreenshot("button-default.png");

  await button.hover();
  await expect(button).toHaveScreenshot("button-hover.png");
});

无障碍测试

// npm install @axe-core/playwright
import AxeBuilder from "@axe-core/playwright";

test("page has no accessibility violations", async ({ page }) => {
  await page.goto("/");

  const results = await new AxeBuilder({ page })
    .exclude("#third-party-widget")  // Exclude things you can't control
    .analyze();

  expect(results.violations).toEqual([]);
});

调试失败的测试

# Run in headed mode (see the browser)
npx playwright test --headed

# Debug mode (step through)
npx playwright test --debug

# Show trace viewer for failed tests
npx playwright show-report
// Add test steps for better failure reports
test("checkout flow", async ({ page }) => {
  await test.step("Add item to cart", async () => {
    await page.goto("/products");
    await page.getByRole("button", { name: "Add to Cart" }).click();
  });

  await test.step("Complete checkout", async () => {
    await page.goto("/checkout");
    // ... if this fails, you know which step
  });
});

// Pause for manual inspection
await page.pause();

不稳定测试检查清单

当测试间歇性失败时,请检查:

问题修复
已修复waitForTimeout()调用替换为waitForSelector()或 expect 断言
页面加载时的竞态条件等待网络空闲或特定元素
测试数据污染确保测试创建/清理自己的数据
动画时序等待动画完成或禁用动画
视口不一致在配置中设置明确的视口
随机测试顺序问题测试必须相互独立
第三方服务不稳定性模拟外部API

CI/CD集成

# GitHub Actions example
name: E2E Tests
on: [push, pull_request]

jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npm run build
      - run: npm run start & npx wait-on http://localhost:3000
      - run: npx playwright test
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/

绝对不要

  1. 绝对不要使用固定的waitForTimeout()cy.wait(ms)——它们会导致测试不稳定并拖慢测试套件
  2. 绝对不要依赖CSS类或DOM结构作为选择器——使用角色、标签或data-testid
  3. 切勿在测试之间共享状态——每个测试必须完全独立
  4. 切勿测试实现细节——测试用户所见和所操作的内容,而非内部结构
  5. 切勿跳过清理步骤——始终删除创建的测试数据,即使测试失败
  6. 切勿对所有内容进行端到端测试——仅针对关键路径使用;对边缘情况采用更快的测试方法
  7. 切勿忽视不稳定的测试——立即修复或删除;不稳定的测试比没有测试更糟糕
  8. 切勿在定位器中硬编码测试数据——对动态内容使用动态等待

快速参考

Playwright 命令

// Navigation
await page.goto("/path");
await page.goBack();
await page.reload();

// Interactions
await page.click("selector");
await page.fill("selector", "text");
await page.type("selector", "text");  // Types character by character
await page.selectOption("select", "value");
await page.check("checkbox");

// Assertions
await expect(page).toHaveURL("/expected");
await expect(locator).toBeVisible();
await expect(locator).toHaveText("expected");
await expect(locator).toBeEnabled();
await expect(locator).toHaveCount(3);

Cypress 命令

// Navigation
cy.visit("/path");
cy.go("back");
cy.reload();

// Interactions
cy.get("selector").click();
cy.get("selector").type("text");
cy.get("selector").clear().type("text");
cy.get("select").select("value");
cy.get("checkbox").check();

// Assertions
cy.url().should("include", "/expected");
cy.get("selector").should("be.visible");
cy.get("selector").should("have.text", "expected");
cy.get("selector").should("have.length", 3);
免责申明
部分文章来自各大搜索引擎,如有侵权,请与我联系删除。
打赏
文章底部电脑广告
手机广告位-内容正文底部
上一篇:Learning 下一篇:Qq Zone Photo

相关文章

您是本站第291259名访客 今日有270篇新文章/评论