frontend-testing-best-practices
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTesting Best Practices
前端测试最佳实践
Guidelines for writing effective, maintainable tests that provide real confidence. Contains 6 rules focused on preferring E2E tests, minimizing mocking, and testing behavior over implementation.
本指南介绍如何编写高效、可维护且能带来真实信心的测试。包含6条核心规则,重点是优先选择E2E测试、尽量减少模拟以及测试行为而非实现细节。
Core Philosophy
核心理念
- Prefer E2E tests over unit tests - Test the whole system, not isolated pieces
- Minimize mocking - If you need complex mocks, write an E2E test instead
- Test behavior, not implementation - Test what users see and do
- Avoid testing React components directly - Test them through E2E
- 优先选择E2E测试而非单元测试 - 测试整个系统,而非孤立模块
- 尽量减少模拟 - 如果需要复杂模拟,改用E2E测试
- 测试行为而非实现细节 - 测试用户所见和操作的内容
- 避免直接测试React组件 - 通过E2E测试覆盖它们
When to Apply
适用场景
Reference these guidelines when:
- Deciding what type of test to write
- Writing new E2E or unit tests
- Reviewing test code
- Refactoring tests
在以下场景参考本指南:
- 决定要编写的测试类型时
- 编写新的E2E或单元测试时
- 评审测试代码时
- 重构测试时
Rules Summary
规则汇总
Testing Strategy (CRITICAL)
测试策略(关键)
prefer-e2e-tests - @rules/prefer-e2e-tests.md
prefer-e2e-tests - @rules/prefer-e2e-tests.md
Default to E2E tests. Only write unit tests for pure functions.
typescript
// E2E test (PREFERRED) - tests real user flow
test("user can place an order", async ({ page }) => {
await createTestingAccount(page, { account_status: "active" });
await page.goto("/catalog");
await page.getByRole("heading", { name: "Example Item" }).click();
await page.getByRole("link", { name: "Buy" }).click();
// ... complete flow
await expect(page.getByAltText("Thank you")).toBeVisible();
});
// Unit test - ONLY for pure functions
test("formatCurrency formats with two decimals", () => {
expect(formatCurrency(1234.5)).toBe("$1,234.50");
});默认优先编写E2E测试。仅为纯函数编写单元测试。
typescript
// E2E test (PREFERRED) - tests real user flow
test("user can place an order", async ({ page }) => {
await createTestingAccount(page, { account_status: "active" });
await page.goto("/catalog");
await page.getByRole("heading", { name: "Example Item" }).click();
await page.getByRole("link", { name: "Buy" }).click();
// ... complete flow
await expect(page.getByAltText("Thank you")).toBeVisible();
});
// Unit test - ONLY for pure functions
test("formatCurrency formats with two decimals", () => {
expect(formatCurrency(1234.5)).toBe("$1,234.50");
});avoid-component-tests - @rules/avoid-component-tests.md
avoid-component-tests - @rules/avoid-component-tests.md
Don't unit test React components. Test them through E2E or not at all.
typescript
// BAD: Component unit test
describe("OrderCard", () => {
test("renders amount", () => {
render(<OrderCard amount={100} />);
expect(screen.getByText("$100")).toBeInTheDocument();
});
});
// GOOD: E2E test covers the component naturally
test("order history shows orders", async ({ page }) => {
await page.goto("/orders");
await expect(page.getByText("$100")).toBeVisible();
});不要对React组件进行单元测试。通过E2E测试覆盖它们,或者不单独测试。
typescript
// BAD: Component unit test
describe("OrderCard", () => {
test("renders amount", () => {
render(<OrderCard amount={100} />);
expect(screen.getByText("$100")).toBeInTheDocument();
});
});
// GOOD: E2E test covers the component naturally
test("order history shows orders", async ({ page }) => {
await page.goto("/orders");
await expect(page.getByText("$100")).toBeVisible();
});minimize-mocking - @rules/minimize-mocking.md
minimize-mocking - @rules/minimize-mocking.md
Keep mocks simple. If you need 3+ mocks, write an E2E test instead.
typescript
// BAD: Too many mocks = write E2E test
vi.mock("~/lib/auth");
vi.mock("~/lib/transactions");
vi.mock("~/hooks/useAccount");
// GOOD: Simple MSW mock for loader test
mockServer.use(
http.get("/api/user", () => HttpResponse.json({ name: "John" })),
);保持模拟简单。如果需要3个以上的模拟,改用E2E测试。
typescript
// BAD: Too many mocks = write E2E test
vi.mock("~/lib/auth");
vi.mock("~/lib/transactions");
vi.mock("~/hooks/useAccount");
// GOOD: Simple MSW mock for loader test
mockServer.use(
http.get("/api/user", () => HttpResponse.json({ name: "John" })),
);E2E Tests (HIGH)
E2E测试(重要)
e2e-test-structure - @rules/e2e-test-structure.md
e2e-test-structure - @rules/e2e-test-structure.md
E2E tests go in , not .
e2e/tests/frontend/typescript
// e2e/tests/order.spec.ts
import { test, expect } from "@playwright/test";
import { addAccountBalance, createTestingAccount } from "./utils";
test.describe("Orders", () => {
test.beforeEach(async ({ page, context }) => {
await createTestingAccount(page, { account_status: "active" });
let cookies = await context.cookies();
let account_id = cookies.find((c) => c.name === "account_id").value;
await addAccountBalance({ account_id, amount: 10000, replaceBalance: true });
});
test("place order with default values", async ({ page }) => {
await page.goto("/catalog");
// ... user flow
});
});E2E测试放在目录下,而非目录。
e2e/tests/frontend/typescript
// e2e/tests/order.spec.ts
import { test, expect } from "@playwright/test";
import { addAccountBalance, createTestingAccount } from "./utils";
test.describe("Orders", () => {
test.beforeEach(async ({ page, context }) => {
await createTestingAccount(page, { account_status: "active" });
let cookies = await context.cookies();
let account_id = cookies.find((c) => c.name === "account_id").value;
await addAccountBalance({ account_id, amount: 10000, replaceBalance: true });
});
test("place order with default values", async ({ page }) => {
await page.goto("/catalog");
// ... user flow
});
});e2e-selectors - @rules/e2e-selectors.md
e2e-selectors - @rules/e2e-selectors.md
Use accessible selectors: role > label > text > testid.
typescript
// GOOD: Role-based (preferred)
await page.getByRole("button", { name: "Submit" }).click();
await page.getByRole("heading", { name: "Dashboard" });
// GOOD: Label-based
await page.getByLabel("Email").fill("test@example.com");
// OK: Test ID when no accessible selector exists
await expect(page.getByTestId("balance")).toHaveText("$1,234");
// BAD: CSS selectors
await page.locator(".btn-primary").click();使用可访问性选择器:角色 > 标签 > 文本 > 测试ID。
typescript
// GOOD: Role-based (preferred)
await page.getByRole("button", { name: "Submit" }).click();
await page.getByRole("heading", { name: "Dashboard" });
// GOOD: Label-based
await page.getByLabel("Email").fill("test@example.com");
// OK: Test ID when no accessible selector exists
await expect(page.getByTestId("balance")).toHaveText("$1,234");
// BAD: CSS selectors
await page.locator(".btn-primary").click();Unit Tests (MEDIUM)
单元测试(中等)
unit-test-structure - @rules/unit-test-structure.md
unit-test-structure - @rules/unit-test-structure.md
Unit tests for pure functions only. Co-locate with source files.
typescript
// app/utils/format.test.ts
import { describe, test, expect } from "vitest";
import { formatCurrency } from "./format";
describe("formatCurrency", () => {
test("formats positive amounts", () => {
expect(formatCurrency(1234.5)).toBe("$1,234.50");
});
test("handles zero", () => {
expect(formatCurrency(0)).toBe("$0.00");
});
});仅为纯函数编写单元测试。与源文件放在同一目录下。
typescript
// app/utils/format.test.ts
import { describe, test, expect } from "vitest";
import { formatCurrency } from "./format";
describe("formatCurrency", () => {
test("formats positive amounts", () => {
expect(formatCurrency(1234.5)).toBe("$1,234.50");
});
test("handles zero", () => {
expect(formatCurrency(0)).toBe("$0.00");
});
});Key Files
关键文件
- - E2E tests (Playwright)
e2e/tests/ - - E2E test utilities
e2e/tests/utils.ts - - Unit test configuration
vitest.config.ts - - Global test setup with MSW
vitest.setup.ts - - Unit test utilities
app/utils/test-utils.ts
- - E2E测试(基于Playwright)
e2e/tests/ - - E2E测试工具类
e2e/tests/utils.ts - - 单元测试配置文件
vitest.config.ts - - 基于MSW的全局测试初始化文件
vitest.setup.ts - - 单元测试工具类
app/utils/test-utils.ts