Loading...
Loading...
Compare original and translation side by side
/ 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 |
/ E2E \ ~10% — 速度慢、不稳定、成本高
/ Integ \ ~20% — 速度中等、使用真实依赖
/ Unit \ ~70% — 速度快、隔离性强、数量多
/______________\| 层级 | 占比 | 速度 | 测试范围 | 运行时机 |
|---|---|---|---|---|
| Unit | ~70% | < 10毫秒 | 单个函数或类 | 每次提交时 |
| Integration | ~20% | < 5秒 | 多个组件协同工作 | 每次推送时 |
| E2E | ~10% | < 60秒 | 完整用户流程 | 预合并、CI阶段 |
| 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 | 在生产代码编写前或编写过程中同步编写测试 |
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);
});
});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 == 108describe("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);
});
});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| 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 |
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 |
| 消息队列 | 基于真实消息代理的发布与消费流程 |
| 文件系统操作 | 临时目录下的读写操作 |
| 缓存层操作 | 基于真实缓存的设置、获取、失效流程 |
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");
});
});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();
});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();
});| 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 | 避免网络依赖与调用限制 |
| 时间与日期函数 | 实现可预测的时间相关测试 |
| 随机数生成器 | 实现可复现的输出结果 |
| 邮件/短信/通知服务 | 避免发送真实消息 |
| 支付处理器 | 测试期间避免产生真实扣费 |
| 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 |
// 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 });| 无需模拟的对象 | 原因 |
|---|---|
| 被测模块本身 | 此时测试的是模拟对象而非实际代码 |
| 简单值对象 | 无外部依赖、无副作用 |
| 标准库函数 | 可信、可预测、速度快 |
| 集成测试中的数据库 | 数据库交互本身就是测试目标 |
| 默认模拟所有对象 | 过度模拟会导致测试通过但实际代码失效 |
// 正确:模拟HTTP客户端而非服务本身
const mockHttpClient = { get: jest.fn().mockResolvedValue({ data: { id: 1, name: "Test" } }) };
const service = new UserService(mockHttpClient);
// 错误:模拟被测方法
jest.spyOn(service, "getUser").mockResolvedValue({ id: 1 });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" });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" });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);
}));
});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);
}));
});| 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 |
| 指标 | 目标值 | 说明 |
|---|---|---|
| 行覆盖率 | >= 80% | 多数项目的合理基准线 |
| 分支覆盖率 | >= 75% | 比行覆盖率更有参考意义 |
| 核心路径覆盖率 | >= 95% | 认证、支付、数据完整性等核心模块 |
| 新增代码覆盖率 | >= 90% | 通过覆盖率差异工具在PR中强制执行 |
| 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 | 修复或删除测试,绝不永久跳过 |
// 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")// 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")