testing-strategy
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTesting Strategy
测试策略
Test Pyramid
测试金字塔
/ E2E \ ~10% — Slow, brittle, expensive
/ Integ \ ~20% — Medium speed, real dependencies
/ Unit \ ~70% — Fast, isolated, numerous
/______________\| Layer | Count | Speed | Scope | Runs |
|---|---|---|---|---|
| Unit | ~70% | < 10ms | Single function or class | Every commit |
| Integration | ~20% | < 5s | Multiple components together | Every push |
| E2E | ~10% | < 60s | Full user workflow | Pre-merge, CI |
If the pyramid inverts (more E2E than unit), treat it as a structural problem.
/ E2E \ ~10% — 速度慢、不稳定、成本高
/ Integ \ ~20% — 速度中等、使用真实依赖
/ Unit \ ~70% — 速度快、隔离性强、数量多
/______________\| 层级 | 占比 | 速度 | 测试范围 | 运行时机 |
|---|---|---|---|---|
| Unit | ~70% | < 10毫秒 | 单个函数或类 | 每次提交时 |
| Integration | ~20% | < 5秒 | 多个组件协同工作 | 每次推送时 |
| E2E | ~10% | < 60秒 | 完整用户流程 | 预合并、CI阶段 |
如果金字塔倒置(E2E测试占比超过单元测试),则视为结构性问题。
Unit Test Principles
单元测试原则
FIRST Properties
FIRST原则
| Property | Rule |
|---|---|
| Fast | Entire unit suite runs in under 30 seconds |
| Isolated | No test depends on another test's execution or state |
| Repeatable | Same result every run, regardless of time, network, or OS |
| Self-validating | Pass or fail with no manual inspection needed |
| Timely | Written before or alongside the production code |
| 原则 | 规则 |
|---|---|
| Fast | 整个单元测试套件运行时间不超过30秒 |
| Isolated | 测试之间不依赖彼此的执行顺序或状态 |
| Repeatable | 无论时间、网络或操作系统如何,每次运行结果一致 |
| Self-validating | 无需人工检查,自动判定通过或失败 |
| Timely | 在生产代码编写前或编写过程中同步编写测试 |
What to Unit Test
单元测试的测试内容
- Pure functions and their edge cases
- Business logic and calculations
- State transitions and validation rules
- Data transformations and mappings
- Error handling paths
- Boundary conditions (empty, null, max, min, off-by-one)
- 纯函数及其边界情况
- 业务逻辑与计算逻辑
- 状态转换与验证规则
- 数据转换与映射
- 错误处理路径
- 边界条件(空值、Null、最大值、最小值、差一错误)
What NOT to Unit Test
无需单元测试的内容
- Framework configuration or boilerplate
- Simple getters/setters with no logic
- Third-party library internals
- Private methods directly (test through public interface)
- 框架配置或样板代码
- 无逻辑的简单getter/setter
- 第三方库内部实现
- 直接测试私有方法(通过公共接口间接测试)
Arrange-Act-Assert
Arrange-Act-Assert
typescript
describe("calculateDiscount", () => {
it("applies 10% discount for orders over $100", () => {
// Arrange
const order = { items: [{ price: 120, quantity: 1 }] };
// Act
const result = calculateDiscount(order);
// Assert
expect(result.discount).toBe(12);
expect(result.total).toBe(108);
});
});python
class TestCalculateDiscount:
def test_applies_10_percent_for_orders_over_100(self):
order = Order(items=[Item(price=120, quantity=1)])
result = calculate_discount(order)
assert result.discount == 12
assert result.total == 108typescript
describe("calculateDiscount", () => {
it("applies 10% discount for orders over $100", () => {
// Arrange
const order = { items: [{ price: 120, quantity: 1 }] };
// Act
const result = calculateDiscount(order);
// Assert
expect(result.discount).toBe(12);
expect(result.total).toBe(108);
});
});python
class TestCalculateDiscount:
def test_applies_10_percent_for_orders_over_100(self):
order = Order(items=[Item(price=120, quantity=1)])
result = calculate_discount(order)
assert result.discount == 12
assert result.total == 108Integration Test Scope
集成测试范围
| Category | Example |
|---|---|
| Database queries | Repository methods with a real (test) database |
| API endpoints | HTTP request through middleware, handler, and response |
| External services | Calls to third-party APIs with sandboxed accounts |
| Message queues | Publish and consume cycle with real broker |
| File system | Read/write operations with temp directories |
| Cache layers | Cache set, get, invalidation with real cache |
typescript
describe("UserRepository", () => {
let db: Database;
beforeAll(async () => { db = await createTestDatabase(); await db.migrate(); });
afterAll(async () => { await db.close(); });
beforeEach(async () => { await db.truncateAll(); });
it("finds user by email", async () => {
await db.insert("users", { email: "test@example.com", name: "Test User" });
const user = await userRepo.findByEmail("test@example.com");
expect(user).not.toBeNull();
expect(user.name).toBe("Test User");
});
});| 类别 | 示例 |
|---|---|
| 数据库查询 | 连接真实(测试)数据库的仓储层方法 |
| API端点 | 经过中间件、处理器的HTTP请求与响应流程 |
| 外部服务调用 | 使用沙箱账号调用第三方API |
| 消息队列 | 基于真实消息代理的发布与消费流程 |
| 文件系统操作 | 临时目录下的读写操作 |
| 缓存层操作 | 基于真实缓存的设置、获取、失效流程 |
typescript
describe("UserRepository", () => {
let db: Database;
beforeAll(async () => { db = await createTestDatabase(); await db.migrate(); });
afterAll(async () => { await db.close(); });
beforeEach(async () => { await db.truncateAll(); });
it("finds user by email", async () => {
await db.insert("users", { email: "test@example.com", name: "Test User" });
const user = await userRepo.findByEmail("test@example.com");
expect(user).not.toBeNull();
expect(user.name).toBe("Test User");
});
});Integration Test Rules
集成测试规则
- Use a dedicated test database, never production data
- Reset state between tests (truncate tables, clear queues)
- Use test containers or in-memory databases where possible
- Test the happy path and the most critical error paths
- 使用专用测试数据库,绝不能使用生产数据
- 测试之间重置状态(清空表、重置队列)
- 尽可能使用测试容器或内存数据库
- 测试正常流程与最关键的错误路径
E2E Test Selection Criteria
E2E测试选择标准
Include: Critical revenue paths (signup, checkout, payment), authentication flows, core user journeys (3-5 things your product must do), cross-system workflows.
Exclude: Edge cases (cover in unit tests), UI-only validation, admin features, scenarios covered by lower-level tests.
typescript
test("user can complete checkout", async ({ page }) => {
await page.goto("/login");
await page.fill('[name="email"]', "user@test.com");
await page.fill('[name="password"]', "password123");
await page.click('button[type="submit"]');
await page.goto("/products/widget-1");
await page.click("text=Add to Cart");
await page.goto("/cart");
await page.click("text=Checkout");
await page.fill('[name="card"]', "4242424242424242");
await page.click("text=Place Order");
await expect(page.locator(".order-confirmation")).toBeVisible();
});需包含: 核心营收路径(注册、结账、支付)、认证流程、核心用户旅程(产品必须具备的3-5项核心功能)、跨系统工作流。
需排除: 边界情况(由单元测试覆盖)、仅UI层面的验证、管理员功能、已由底层测试覆盖的场景。
typescript
test("user can complete checkout", async ({ page }) => {
await page.goto("/login");
await page.fill('[name="email"]', "user@test.com");
await page.fill('[name="password"]', "password123");
await page.click('button[type="submit"]');
await page.goto("/products/widget-1");
await page.click("text=Add to Cart");
await page.goto("/cart");
await page.click("text=Checkout");
await page.fill('[name="card"]', "4242424242424242");
await page.click("text=Place Order");
await expect(page.locator(".order-confirmation")).toBeVisible();
});Mocking Guidelines
模拟(Mock)指南
When to Mock
何时使用模拟
| Mock | Reason |
|---|---|
| External HTTP APIs | Avoid network dependency and rate limits |
| Time and date functions | Enable deterministic time-based tests |
| Random number generators | Enable reproducible outputs |
| Email/SMS/notification services | Prevent sending real messages |
| Payment processors | Avoid real charges during tests |
| 模拟对象 | 原因 |
|---|---|
| 外部HTTP API | 避免网络依赖与调用限制 |
| 时间与日期函数 | 实现可预测的时间相关测试 |
| 随机数生成器 | 实现可复现的输出结果 |
| 邮件/短信/通知服务 | 避免发送真实消息 |
| 支付处理器 | 测试期间避免产生真实扣费 |
When NOT to Mock
何时不使用模拟
| Do Not Mock | Reason |
|---|---|
| The module under test | You would be testing your mock, not your code |
| Simple value objects | No external dependency, no side effects |
| Standard library functions | Trusted, deterministic, fast |
| Database in integration | The database interaction is what you are testing |
| Everything by default | Over-mocking makes tests pass even when code breaks |
typescript
// Good: mock the HTTP client, not the service
const mockHttpClient = { get: jest.fn().mockResolvedValue({ data: { id: 1, name: "Test" } }) };
const service = new UserService(mockHttpClient);
// Bad: mocking the method you are testing
jest.spyOn(service, "getUser").mockResolvedValue({ id: 1 });| 无需模拟的对象 | 原因 |
|---|---|
| 被测模块本身 | 此时测试的是模拟对象而非实际代码 |
| 简单值对象 | 无外部依赖、无副作用 |
| 标准库函数 | 可信、可预测、速度快 |
| 集成测试中的数据库 | 数据库交互本身就是测试目标 |
| 默认模拟所有对象 | 过度模拟会导致测试通过但实际代码失效 |
typescript
// 正确:模拟HTTP客户端而非服务本身
const mockHttpClient = { get: jest.fn().mockResolvedValue({ data: { id: 1, name: "Test" } }) };
const service = new UserService(mockHttpClient);
// 错误:模拟被测方法
jest.spyOn(service, "getUser").mockResolvedValue({ id: 1 });Test Fixtures and Factories
测试固件与工厂
Prefer factories over shared fixtures to keep tests independent.
typescript
function buildUser(overrides: Partial<User> = {}): User {
return {
id: randomUUID(),
email: `user-${Date.now()}@test.com`,
name: "Test User",
role: "member",
createdAt: new Date(),
...overrides,
};
}
const admin = buildUser({ role: "admin" });- Generate unique values to prevent cross-test contamination
- Use factories with sensible defaults and explicit overrides
- Keep test data minimal; only set fields relevant to the assertion
- Never share mutable state between tests
优先使用工厂模式而非共享固件,以保持测试独立性。
typescript
function buildUser(overrides: Partial<User> = {}): User {
return {
id: randomUUID(),
email: `user-${Date.now()}@test.com`,
name: "Test User",
role: "member",
createdAt: new Date(),
...overrides,
};
}
const admin = buildUser({ role: "admin" });- 生成唯一值以避免测试间数据污染
- 使用带有合理默认值和显式覆盖的工厂
- 测试数据尽可能精简,仅设置与断言相关的字段
- 绝不在测试间共享可变状态
Property-Based Testing
属性化测试
Test invariants with randomly generated inputs.
typescript
import fc from "fast-check";
test("sort is idempotent", () => {
fc.assert(fc.property(fc.array(fc.integer()), (arr) => {
expect(sort(sort(arr))).toEqual(sort(arr));
}));
});
test("encode/decode roundtrip", () => {
fc.assert(fc.property(fc.string(), (input) => {
expect(decode(encode(input))).toBe(input);
}));
});Use for: serialization roundtrips, sorting invariants, mathematical properties, parsers, encoding.
使用随机生成的输入测试不变量。
typescript
import fc from "fast-check";
test("sort is idempotent", () => {
fc.assert(fc.property(fc.array(fc.integer()), (arr) => {
expect(sort(sort(arr))).toEqual(sort(arr));
}));
});
test("encode/decode roundtrip", () => {
fc.assert(fc.property(fc.string(), (input) => {
expect(decode(encode(input))).toBe(input);
}));
});适用场景:序列化往返测试、排序不变量、数学属性、解析器、编码逻辑。
Snapshot Testing
快照测试
Appropriate: Serialized output formats, generated config files, stable component render output.
Harmful: Large frequently-changing outputs, UI with dynamic content, testing logic or behavior.
- Review snapshot diffs carefully; never blindly update
- Keep snapshots small and focused
- Use inline snapshots for short outputs
适用场景: 序列化输出格式、生成的配置文件、稳定的组件渲染输出。
不适用场景: 频繁变更的大体积输出、含动态内容的UI、逻辑或行为测试。
- 仔细检查快照差异,绝不盲目更新
- 保持快照精简且聚焦
- 短输出使用内联快照
Coverage Metrics
覆盖率指标
| Metric | Target | Notes |
|---|---|---|
| Line coverage | >= 80% | Reasonable baseline for most projects |
| Branch coverage | >= 75% | More meaningful than line coverage |
| Critical path | >= 95% | Auth, payments, data integrity |
| New code coverage | >= 90% | Enforce on PRs with coverage diff tools |
- Track coverage trends over time, not just absolute numbers
- Never game coverage with meaningless tests
- 100% coverage does not mean bug-free; focus on meaningful assertions
- Exclude generated code, vendor code, and config from metrics
| 指标 | 目标值 | 说明 |
|---|---|---|
| 行覆盖率 | >= 80% | 多数项目的合理基准线 |
| 分支覆盖率 | >= 75% | 比行覆盖率更有参考意义 |
| 核心路径覆盖率 | >= 95% | 认证、支付、数据完整性等核心模块 |
| 新增代码覆盖率 | >= 90% | 通过覆盖率差异工具在PR中强制执行 |
- 跟踪覆盖率趋势而非仅关注绝对数值
- 绝不通过无意义测试刷覆盖率
- 100%覆盖率不代表无bug,聚焦有意义的断言
- 覆盖率指标排除生成代码、第三方代码与配置文件
Testing Anti-Patterns
测试反模式
| Anti-Pattern | Problem | Fix |
|---|---|---|
| Testing implementation | Breaks when refactoring, even if behavior is same | Test behavior and outputs |
| Flaky tests | Erodes trust, gets ignored | Fix or delete immediately |
| Slow test suite | Developers skip running tests | Parallelize, mock I/O, split by layer |
| Over-mocking | Tests pass but code is broken | Mock boundaries only |
| Shared mutable state | Tests pass alone, fail together | Isolate state per test, use factories |
| Testing private methods | Couples tests to implementation details | Test through the public API |
| No assertion | Test passes without verifying anything | Every test must assert something |
| Ignoring test failures | Hides real bugs | Fix or remove; never skip permanently |
| 反模式 | 问题 | 修复方案 |
|---|---|---|
| 测试实现细节 | 重构时即使行为不变也会导致测试失败 | 测试行为与输出结果 |
| 不稳定测试(Flaky) | 降低测试可信度,被开发者忽略 | 立即修复或删除 |
| 测试套件速度慢 | 开发者会跳过运行测试 | 并行执行、模拟I/O、按层级拆分测试套件 |
| 过度模拟 | 测试通过但实际代码失效 | 仅在边界层使用模拟 |
| 共享可变状态 | 单独测试通过但组合测试失败 | 每个测试隔离状态、使用工厂模式 |
| 测试私有方法 | 测试与实现细节耦合 | 通过公共接口间接测试 |
| 无断言测试 | 未验证任何内容就标记通过 | 每个测试必须包含断言 |
| 忽略测试失败 | 隐藏真实bug | 修复或删除测试,绝不永久跳过 |
Test Naming Conventions
测试命名规范
// describe/it style
describe("ShoppingCart")
it("calculates total with tax for items in cart")
// given/when/then style
test("given an empty cart, when adding an item, then total reflects item price")
// should style
test("should return 404 when user is not found")- Name describes the scenario and expected outcome
- Name reads as a sentence or specification
- Failed test name tells you what broke without reading the code
// describe/it 风格
describe("ShoppingCart")
it("calculates total with tax for items in cart")
// given/when/then 风格
test("given an empty cart, when adding an item, then total reflects item price")
// should 风格
test("should return 404 when user is not found")- 名称需描述测试场景与预期结果
- 名称应像句子或规格说明一样易读
- 测试失败时,仅看名称就能知道问题所在,无需查看代码