frontend-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Frontend Testing

前端测试

Start by writing tests that validate acceptance criteria. Then add implementation tests where they provide value.
编写测试时,先验证验收标准,再在有价值的场景下添加实现测试。

Core Principle

核心原则

"The more your tests resemble the way your software is used, the more confidence they can give you." — Kent C. Dodds
This principle guides testing decisions, but isn't the whole picture:
  • Acceptance criteria tests verify the system does what users/stakeholders need. These should be stable across refactors.
  • Implementation tests verify the pieces are robust — edge cases, error handling, complex logic. These may change when you refactor.
Both have value. The anti-pattern to avoid is tests that only mirror implementation without validating meaningful behavior.
"你的测试越贴近软件的实际使用方式,就能给你带来越高的信心。" — Kent C. Dodds
这一原则指导测试决策,但并非全部:
  • 验收标准测试:验证系统是否满足用户/利益相关者的需求。这类测试在重构过程中应保持稳定。
  • 实现测试:验证各个模块的健壮性——包括边缘情况、错误处理、复杂逻辑。这类测试可能会在重构时发生变化。
两者都有价值。需要避免的反模式是:仅镜像实现细节,却不验证有意义的行为的测试。

When to Load References

何时加载参考文档

Load reference files based on test type:
  • Unit tests with DOM:
    references/locator-strategies.md
  • E2E tests:
    references/locator-strategies.md
    ,
    references/aria-snapshots.md
  • Visual regression tests:
    references/visual-regression.md
  • Accessibility audits:
    references/accessibility-testing.md
  • Structure validation:
    references/aria-snapshots.md
    — consolidate multiple assertions into one
  • All tests: Start with this file for core workflow
根据测试类型加载参考文件:
  • 带DOM的单元测试
    references/locator-strategies.md
  • 端到端测试
    references/locator-strategies.md
    references/aria-snapshots.md
  • 视觉回归测试
    references/visual-regression.md
  • 可访问性审计
    references/accessibility-testing.md
  • 结构验证
    references/aria-snapshots.md
    ——将多个断言合并为一个
  • 所有测试:从本文档开始了解核心工作流

Workflow

工作流

Step 1: Start with Acceptance Criteria

步骤1:从验收标准开始

Before writing any test, identify what the code should do from the user's perspective.
Ask for or extract criteria from:
  • Ticket description or user story
  • Figma annotations
  • Functional requirements
  • Product owner clarification
Document criteria as a checklist. These become your first tests.
Write acceptance tests before reading implementation. This prevents circular validation where tests just confirm "code does what code does."
在编写任何测试之前,从用户视角明确代码应实现的功能。
从以下渠道获取或提取验收标准:
  • 工单描述或用户故事
  • Figma标注
  • 功能需求
  • 产品负责人的说明
将标准整理为检查清单,这些将成为你的第一批测试。
在查看实现代码之前编写验收测试。这可以避免循环验证——即测试仅确认“代码做了代码做的事”。

Step 2: Map Criteria to Test Cases

步骤2:将标准映射到测试用例

For each criterion, identify:
  • Happy path: Normal expected behavior
  • Edge cases: Boundary conditions
  • Error cases: Invalid inputs, failures
Example mapping:
Criterion: "User can filter products by category"
├─ Happy path: Select category, products filter correctly
├─ Edge case: No products match filter, show empty state
├─ Edge case: Clear filter, all products show again
├─ Error case: Filter API fails, show error message
└─ Accessibility: Filter controls are keyboard accessible
针对每个验收标准,确定:
  • 正常路径:预期的常规行为
  • 边缘情况:边界条件
  • 错误情况:无效输入、故障场景
示例映射:
标准:"用户可按类别筛选产品"
├─ 正常路径:选择类别后,产品正确筛选
├─ 边缘情况:无匹配产品时显示空状态
├─ 边缘情况:清除筛选后显示所有产品
├─ 错误情况:筛选API失败时显示错误提示
└─ 可访问性:筛选控件支持键盘操作

Step 3: Add Implementation Tests (Unit Tests)

步骤3:添加实现测试(单元测试)

After acceptance tests pass, add unit tests for implementation robustness:
  • Edge cases the criteria don't cover (null, undefined, empty arrays, boundary values)
  • Algorithm correctness for complex calculations
  • Error handling paths (exceptions, network failures, parse errors)
  • Complex branching logic hard to exercise through integration tests
  • Performance-sensitive code that needs specific validation
Function: filterProducts(products, category)
├─ Acceptance: Returns matching products (from criteria)
├─ Implementation: Returns empty array when products is null
├─ Implementation: Returns all products when category is empty string
├─ Implementation: Handles case-insensitive category matching
└─ Implementation: Does not mutate original array
The distinction: acceptance tests should rarely change on refactor; implementation tests may need updates when internals change.
验收测试通过后,添加实现测试以确保模块健壮性:
  • 验收标准未覆盖的边缘情况(null、undefined、空数组、边界值)
  • 复杂计算的算法正确性
  • 错误处理路径(异常、网络故障、解析错误)
  • 难以通过集成测试覆盖的复杂分支逻辑
  • 需要特定验证的性能敏感代码
函数:filterProducts(products, category)
├─ 验收测试:返回匹配的产品(来自标准)
├─ 实现测试:当products为null时返回空数组
├─ 实现测试:当category为空字符串时返回所有产品
├─ 实现测试:支持类别名称的大小写不敏感匹配
└─ 实现测试:不修改原始数组
区别在于:验收测试在重构时几乎不需要修改;实现测试可能需要在内部逻辑变更时更新。

Step 4: Choose Test Type

步骤4:选择测试类型

ScenarioTest TypeTool
Pure logic (no DOM)Unit testVitest
Component behaviorUnit test with DOMVitest + Testing Library
User flows, real browserE2E testPlaywright
Semantic structure validationARIA snapshotPlaywright
toMatchAriaSnapshot
Visual appearanceVRTPlaywright screenshots
Accessibility complianceA11y testPlaywright + axe-core
ARIA snapshots are particularly valuable for E2E tests. A single snapshot can replace multiple individual assertions while validating the accessibility tree structure.
DOM Environment for Unit Tests: Prefer happy-dom over jsdom. It's faster, and its API limitations serve as a useful signal — if happy-dom doesn't support what you're testing, consider whether it belongs in an E2E test instead.
场景测试类型工具
纯逻辑(无DOM)单元测试Vitest
组件行为带DOM的单元测试Vitest + Testing Library
用户流程、真实浏览器环境端到端测试Playwright
语义结构验证ARIA快照Playwright
toMatchAriaSnapshot
视觉外观视觉回归测试(VRT)Playwright 截图
可访问性合规可访问性测试Playwright + axe-core
ARIA快照在端到端测试中尤其有价值。单个快照可以替代多个独立断言,同时验证可访问性树结构。
单元测试的DOM环境:优先使用happy-dom而非jsdom。它速度更快,且其API限制是一个有用的信号——如果happy-dom不支持你要测试的内容,考虑是否应该改用端到端测试。

Step 5: Write Tests Before/Alongside Code

步骤5:在编写代码之前/同时编写测试

  • Ideal: Write test first, then implementation (TDD)
  • Acceptable: Write test immediately after implementing each criterion
  • Avoid: Write all tests after implementation is "done"
  • 理想方式:先写测试,再实现代码(测试驱动开发TDD)
  • 可接受方式:实现每个标准后立即编写测试
  • 避免:等所有实现“完成”后再写测试

Test Structure

测试结构

Unit Tests (Vitest)

单元测试(Vitest)

javascript
describe("calculateDiscount", () => {
  describe("when customer has premium membership", () => {
    it("applies 20% discount to order total", () => {
      // Arrange - Set up test data matching criterion
      const order = { total: 100, membership: "premium" };
      
      // Act - Call the function
      const result = calculateDiscount(order);
      
      // Assert - Verify expected outcome from requirements
      expect(result).toBe(80);
    });
  });
});
javascript
describe("calculateDiscount", () => {
  describe("when customer has premium membership", () => {
    it("applies 20% discount to order total", () => {
      // Arrange - 设置符合标准的测试数据
      const order = { total: 100, membership: "premium" };
      
      // Act - 调用函数
      const result = calculateDiscount(order);
      
      // Assert - 验证需求中的预期结果
      expect(result).toBe(80);
    });
  });
});

Component Tests (Vitest + Testing Library)

组件测试(Vitest + Testing Library)

javascript
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

describe("LoginForm", () => {
  describe("when credentials are invalid", () => {
    it("displays error message to user", async () => {
      const user = userEvent.setup();
      render(<LoginForm />);
      
      // Interact using accessible queries
      await user.type(
        screen.getByLabelText(/email/i),
        "invalid@test.com"
      );
      await user.type(
        screen.getByLabelText(/password/i),
        "wrong"
      );
      await user.click(
        screen.getByRole("button", { name: /sign in/i })
      );
      
      // Assert on user-visible outcome
      expect(
        await screen.findByRole("alert")
      ).toHaveTextContent(/invalid credentials/i);
    });
  });
});
javascript
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

describe("LoginForm", () => {
  describe("when credentials are invalid", () => {
    it("displays error message to user", async () => {
      const user = userEvent.setup();
      render(<LoginForm />);
      
      // 使用可访问性查询进行交互
      await user.type(
        screen.getByLabelText(/email/i),
        "invalid@test.com"
      );
      await user.type(
        screen.getByLabelText(/password/i),
        "wrong"
      );
      await user.click(
        screen.getByRole("button", { name: /sign in/i })
      );
      
      // 断言用户可见的结果
      expect(
        await screen.findByRole("alert")
      ).toHaveTextContent(/invalid credentials/i);
    });
  });
});

E2E Tests (Playwright)

端到端测试(Playwright)

javascript
import { test, expect } from "@playwright/test";

test.describe("Product Catalog", () => {
  test.describe("filtering by category", () => {
    test("shows only matching products", async ({ page }) => {
      await page.goto("/products");
      
      // Use semantic locators
      await page.getByRole("combobox", { name: /category/i }).selectOption("Electronics");
      
      // Assert count, then spot-check first/last
      const products = page.getByRole("article");
      await expect(products).toHaveCount(5);
      await expect(products.first()).toContainText(/electronics/i);
      await expect(products.last()).toContainText(/electronics/i);
    });
  });
});
When you need to verify all items, use
Promise.all
for parallel assertions:
javascript
test("all products match filter", async ({ page }) => {
  await page.goto("/products");
  await page.getByRole("combobox", { name: /category/i }).selectOption("Electronics");
  
  const products = await page.getByRole("article").all();
  
  // Parallel assertions — faster than sequential await in a loop
  await Promise.all(
    products.map(product =>
      expect(product.getByText(/electronics/i)).toBeVisible()
    )
  );
});
javascript
import { test, expect } from "@playwright/test";

test.describe("Product Catalog", () => {
  test.describe("filtering by category", () => {
    test("shows only matching products", async ({ page }) => {
      await page.goto("/products");
      
      // 使用语义定位器
      await page.getByRole("combobox", { name: /category/i }).selectOption("Electronics");
      
      // 断言数量,然后抽查第一个/最后一个
      const products = page.getByRole("article");
      await expect(products).toHaveCount(5);
      await expect(products.first()).toContainText(/electronics/i);
      await expect(products.last()).toContainText(/electronics/i);
    });
  });
});
当需要验证所有项时,使用
Promise.all
进行并行断言:
javascript
test("all products match filter", async ({ page }) => {
  await page.goto("/products");
  await page.getByRole("combobox", { name: /category/i }).selectOption("Electronics");
  
  const products = await page.getByRole("article").all();
  
  // 并行断言 — 比循环中顺序执行更快
  await Promise.all(
    products.map(product =>
      expect(product.getByText(/electronics/i)).toBeVisible()
    )
  );
});

E2E Tests with ARIA Snapshots

带ARIA快照的端到端测试

ARIA snapshots consolidate multiple assertions into one, validating semantic structure:
javascript
test.describe("Login Page", () => {
  test("has correct form structure", async ({ page }) => {
    await page.goto("/login");
    
    // One snapshot replaces 5+ individual assertions
    await expect(page.getByRole("main")).toMatchAriaSnapshot(`
      - heading "Sign In" [level=1]
      - textbox "Email"
      - textbox "Password"
      - button "Sign In"
      - link "Forgot password?"
    `);
  });
  
  test("shows validation errors on empty submit", async ({ page }) => {
    await page.goto("/login");
    await page.getByRole("button", { name: /sign in/i }).click();
    
    await expect(page.getByRole("form")).toMatchAriaSnapshot(`
      - textbox "Email"
      - text "Email is required"
      - textbox "Password"
      - text "Password is required"
      - button "Sign In"
    `);
  });
});
ARIA快照将多个断言合并为一个,验证语义结构:
javascript
test.describe("Login Page", () => {
  test("has correct form structure", async ({ page }) => {
    await page.goto("/login");
    
    // 一个快照替代5个以上的独立断言
    await expect(page.getByRole("main")).toMatchAriaSnapshot(`
      - heading "Sign In" [level=1]
      - textbox "Email"
      - textbox "Password"
      - button "Sign In"
      - link "Forgot password?"
    `);
  });
  
  test("shows validation errors on empty submit", async ({ page }) => {
    await page.goto("/login");
    await page.getByRole("button", { name: /sign in/i }).click();
    
    await expect(page.getByRole("form")).toMatchAriaSnapshot(`
      - textbox "Email"
      - text "Email is required"
      - textbox "Password"
      - text "Password is required"
      - button "Sign In"
    `);
  });
});

Locator Priority

定位器优先级

Use locators that reflect how users and assistive technologies find elements:
  1. getByRole
    — First choice. Queries accessibility tree.
  2. getByLabelText
    — Best for form fields. Users find inputs by labels.
  3. getByPlaceholderText
    — When no label exists (not ideal).
  4. getByText
    — For non-interactive elements.
  5. getByAltText
    — For images.
  6. getByTestId
    — Last resort escape hatch.
If you can't find an element with semantic queries, the UI may have accessibility issues.
使用反映用户和辅助技术查找元素方式的定位器:
  1. getByRole
    — 首选。查询可访问性树。
  2. getByLabelText
    — 表单字段的最佳选择。用户通过标签查找输入框。
  3. getByPlaceholderText
    — 当没有标签时使用(非理想情况)。
  4. getByText
    — 用于非交互式元素。
  5. getByAltText
    — 用于图片。
  6. getByTestId
    — 最后的应急方案。
如果无法通过语义查询找到元素,说明UI可能存在可访问性问题。

Anti-Patterns

反模式

Testing Only Implementation Details

仅测试实现细节

Tests that only verify internals without validating meaningful behavior:
javascript
// BAD: Tests internal method exists, provides no behavior guarantee
it("has a validateFields method", () => {
  expect(form.#validateFields).toBeDefined();
});

// BAD: Asserts implementation without verifying outcome
it("calls the validator", () => {
  expect(mockValidator).toHaveBeenCalledWith(data);
});

// GOOD: Tests observable behavior (acceptance)
it("prevents submission with invalid email", async () => {
  await user.type(emailInput, "not-an-email");
  await user.click(submitButton);
  expect(screen.getByRole("alert")).toHaveTextContent(/valid email/i);
});

// ALSO GOOD: Tests implementation robustness (unit)
it("returns validation errors for malformed email", () => {
  const result = validateEmail("not-an-email");
  expect(result.valid).toBe(false);
  expect(result.error).toBe("Invalid email format");
});
The key distinction: implementation tests should verify meaningful behavior of units, not just that code paths execute.
仅验证内部逻辑而不验证有意义行为的测试:
javascript
// 错误示例:测试内部方法是否存在,无法提供行为保证
it("has a validateFields method", () => {
  expect(form.#validateFields).toBeDefined();
});

// 错误示例:断言实现而非验证结果
it("calls the validator", () => {
  expect(mockValidator).toHaveBeenCalledWith(data);
});

// 正确示例:测试可观察行为(验收测试)
it("prevents submission with invalid email", async () => {
  await user.type(emailInput, "not-an-email");
  await user.click(submitButton);
  expect(screen.getByRole("alert")).toHaveTextContent(/valid email/i);
});

// 同样正确:测试实现的健壮性(单元测试)
it("returns validation errors for malformed email", () => {
  const result = validateEmail("not-an-email");
  expect(result.valid).toBe(false);
  expect(result.error).toBe("Invalid email format");
});
关键区别:实现测试应验证单元的有意义行为,而不仅仅是代码路径是否执行。

Circular Validation

循环验证

javascript
// BAD: Test data derived from implementation
const expected = formatPrice(100); // Don't compute expected from code!
expect(formatPrice(100)).toBe(expected);

// GOOD: Expected value from requirements
expect(formatPrice(100)).toBe("$100.00");
javascript
// 错误示例:测试数据来自实现代码
const expected = formatPrice(100); // 不要从代码计算预期值!
expect(formatPrice(100)).toBe(expected);

// 正确示例:预期值来自需求
expect(formatPrice(100)).toBe("$100.00");

Over-Mocking

过度Mock

javascript
// BAD: Mock everything, test nothing real
jest.mock("./api");
jest.mock("./utils");
jest.mock("./formatter");
// Now just testing mocks talk to each other

// GOOD: Mock only external boundaries
// Mock: APIs, databases, time, file system
// Real: Business logic, components, formatters
javascript
// 错误示例:Mock所有内容,实际未测试任何真实逻辑
jest.mock("./api");
jest.mock("./utils");
jest.mock("./formatter");
// 现在只是测试Mock之间的交互

// 正确示例:仅Mock外部边界
// Mock:API、数据库、时间、文件系统
// 真实测试:业务逻辑、组件、格式化器

Brittle Selectors

脆弱的选择器

javascript
// BAD: Implementation-dependent selectors
page.locator(".btn-primary.submit-form");
page.locator("#root > div > form > button:nth-child(3)");

// GOOD: Semantic locators
page.getByRole("button", { name: /submit order/i });
javascript
// 错误示例:依赖实现的选择器
page.locator(".btn-primary.submit-form");
page.locator("#root > div > form > button:nth-child(3)");

// 正确示例:语义定位器
page.getByRole("button", { name: /submit order/i });

Failing Tests Mean Bugs (Usually)

测试失败通常意味着Bug

When a test fails, the investigation depends on the test type:
Acceptance test fails:
  1. Check the code under test first — likely a real bug
  2. Verify the test still matches current requirements — requirements may have changed
  3. Only update the test after confirming the new behavior is correct
Implementation test fails:
  1. If you're refactoring, the test may legitimately need updating
  2. If you're not refactoring, check the code — likely a bug
  3. Consider whether the test is too tightly coupled to implementation
Don't reflexively update tests to pass. Investigate why they fail.
当测试失败时,调查方向取决于测试类型:
验收测试失败
  1. 首先检查被测代码——很可能是真实Bug
  2. 验证测试是否仍符合当前需求——需求可能已变更
  3. 只有在确认新行为正确后,才更新测试
实现测试失败
  1. 如果正在重构,测试可能确实需要更新
  2. 如果未进行重构,检查代码——很可能是Bug
  3. 考虑测试是否与实现耦合过紧
不要下意识地更新测试使其通过。先调查失败原因。

Checklist Before Submitting Tests

提交测试前的检查清单

  • Each acceptance criterion has at least one test
  • Edge cases from criteria are covered
  • Implementation tests added for complex logic, error handling, boundary conditions
  • Tests are named descriptively (criteria-based or behavior-based)
  • Using semantic locators (getByRole, getByLabelText)
  • No tests that only verify "code runs" without validating meaningful behavior
  • Failing tests investigated appropriately (acceptance vs implementation)
  • Accessibility checks included for interactive components
  • Consider ARIA snapshots for structure validation (consolidates multiple assertions)
  • 每个验收标准至少对应一个测试
  • 覆盖了标准中的边缘情况
  • 为复杂逻辑、错误处理、边界条件添加了实现测试
  • 测试命名具有描述性(基于标准或行为)
  • 使用了语义定位器(getByRole、getByLabelText)
  • 没有仅验证“代码能运行”而不验证有意义行为的测试
  • 已针对失败测试进行适当调查(验收测试vs实现测试)
  • 为交互式组件添加了可访问性检查
  • 考虑使用ARIA快照进行结构验证(合并多个断言)

References

参考文档

Load these files for detailed guidance:
  • references/locator-strategies.md
    — Complete locator hierarchy with examples
  • references/aria-snapshots.md
    — Structure validation, consolidating assertions
  • references/accessibility-testing.md
    — axe-core integration, WCAG targeting
  • references/visual-regression.md
    — Screenshot testing, baseline management
加载以下文件获取详细指导:
  • references/locator-strategies.md
    — 完整的定位器层级及示例
  • references/aria-snapshots.md
    — 结构验证、断言合并
  • references/accessibility-testing.md
    — axe-core集成、WCAG合规
  • references/visual-regression.md
    — 截图测试、基线管理