testing-strategy

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Testing Strategy

测试策略

Test Pyramid

测试金字塔

        /  E2E  \          ~10% — Slow, brittle, expensive
       /  Integ  \         ~20% — Medium speed, real dependencies
      /   Unit    \        ~70% — Fast, isolated, numerous
     /______________\
LayerCountSpeedScopeRuns
Unit~70%< 10msSingle function or classEvery commit
Integration~20%< 5sMultiple components togetherEvery push
E2E~10%< 60sFull user workflowPre-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原则

PropertyRule
FastEntire unit suite runs in under 30 seconds
IsolatedNo test depends on another test's execution or state
RepeatableSame result every run, regardless of time, network, or OS
Self-validatingPass or fail with no manual inspection needed
TimelyWritten 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 == 108
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 == 108

Integration Test Scope

集成测试范围

CategoryExample
Database queriesRepository methods with a real (test) database
API endpointsHTTP request through middleware, handler, and response
External servicesCalls to third-party APIs with sandboxed accounts
Message queuesPublish and consume cycle with real broker
File systemRead/write operations with temp directories
Cache layersCache 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

何时使用模拟

MockReason
External HTTP APIsAvoid network dependency and rate limits
Time and date functionsEnable deterministic time-based tests
Random number generatorsEnable reproducible outputs
Email/SMS/notification servicesPrevent sending real messages
Payment processorsAvoid real charges during tests
模拟对象原因
外部HTTP API避免网络依赖与调用限制
时间与日期函数实现可预测的时间相关测试
随机数生成器实现可复现的输出结果
邮件/短信/通知服务避免发送真实消息
支付处理器测试期间避免产生真实扣费

When NOT to Mock

何时不使用模拟

Do Not MockReason
The module under testYou would be testing your mock, not your code
Simple value objectsNo external dependency, no side effects
Standard library functionsTrusted, deterministic, fast
Database in integrationThe database interaction is what you are testing
Everything by defaultOver-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

覆盖率指标

MetricTargetNotes
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-PatternProblemFix
Testing implementationBreaks when refactoring, even if behavior is sameTest behavior and outputs
Flaky testsErodes trust, gets ignoredFix or delete immediately
Slow test suiteDevelopers skip running testsParallelize, mock I/O, split by layer
Over-mockingTests pass but code is brokenMock boundaries only
Shared mutable stateTests pass alone, fail togetherIsolate state per test, use factories
Testing private methodsCouples tests to implementation detailsTest through the public API
No assertionTest passes without verifying anythingEvery test must assert something
Ignoring test failuresHides real bugsFix 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")
  • 名称需描述测试场景与预期结果
  • 名称应像句子或规格说明一样易读
  • 测试失败时,仅看名称就能知道问题所在,无需查看代码