Skip to content
返回质量保障

单元测试

隔离测试最小逻辑单元,快速且确定性。

为什么

单元测试是抵御回归的第一道防线。它们在毫秒级运行,每天在 CI 中执行数千次,失败时精确定位到出错的函数。没有它们,每次重构都是赌博——你改了一个共享工具函数,然后祈祷下游没坏,因为唯一的反馈是手动点击应用。有强单元覆盖的团队发布更快,因为他们信任测试能抓住明显的破坏,从而把手动测试的精力集中在自动化无法触及的微妙探索路径上。

何时使用

一旦有了纯函数或含逻辑的类就写第一个单元测试。每修一个 bug 加一条(回归守卫),每个新公开方法加一条。业务逻辑目标 70-80% 行覆盖;跳过平凡的 getter/setter。

如何做

1. 选匹配技术栈的框架
   - JS/TS: Vitest(快、Vite 原生)或 Jest
   - Python: pytest + fixtures
   - Go: 内置 testing 包 + testify
   - Rust: cargo test + #[test]

2. 每个测试按 Arrange-Act-Assert 结构
   - Arrange: 准备输入和依赖(mock/stub)
   - Act: 调用被测函数,捕获结果
   - Assert: 验证结果符合预期
   - 每个测试一个逻辑断言;用描述性命名

3. 隔离外部依赖
   - mock 网络调用、数据库、文件系统、时间
   - 用依赖注入让真实和假实现可互换
   - 绝不 mock 你正在测的东西——mock 它的协作者

4. 每次保存(watch 模式)和每次 CI 推送都跑
   - vitest --watch / pytest-watch / cargo watch -x test
   - CI: 任何红测试即构建失败,不许"以后再修"

5. 测覆盖率但别追 100%
   - 生成报告,看业务逻辑中未覆盖的分支
   - 忽略入口点、配置和平凡代码的覆盖率

示例

// src/lib/discount.ts
export function applyDiscount(price: number, code: string): number {
  if (code === "SAVE10") return round(price * 0.9);
  if (code === "SAVE20") return round(price * 0.8);
  return price;
}
const round = (n: number) => Math.round(n * 100) / 100;

// src/lib/__tests__/discount.test.ts
import { describe, it, expect } from "vitest";
import { applyDiscount } from "../discount";

describe("applyDiscount", () => {
  it("SAVE10 打 9 折", () => {
    expect(applyDiscount(100, "SAVE10")).toBe(90);
  });
  it("SAVE20 打 8 折", () => {
    expect(applyDiscount(100, "SAVE20")).toBe(80);
  });
  it("未知码返回原价", () => {
    expect(applyDiscount(100, "NOPE")).toBe(100);
  });
  it("四舍五入到 2 位小数", () => {
    expect(applyDiscount(33.33, "SAVE10")).toBe(30);
  });
});

常见问题

  • ⚠依赖执行顺序或共享可变状态的测试单独跑通过但换序就挂——始终在 beforeEach 重置状态,绝不依赖前一个测试已运行。
  • ⚠mock 太深导致测试断言实现以特定顺序调特定方法,任何无害重构都会挂测试,即使行为没变。
  • ⚠只测快乐路径让边界(null、空数组、负数、并发调用)无人守卫,而这些正是生产 bug 藏身处。
  • ⚠慢单元测试(每条超 100ms)通常意味着真在打数据库或网络——套件涨到分钟级,开发者不再本地跑,回归就溜进来。

最佳实践

  • ✓把测试名当描述行为的句子:"SAVE10 返回 90"读起来像规格;"test1"或"discountTest"在凌晨 2 点 CI 挂了啥也看不出来。
  • ✓每条测试保持在 50ms 以下;如果更慢,你大概在碰 I/O——把逻辑抽成纯函数再测它。
  • ✓对多输入输出对的函数用表驱动测试:一条参数化测试带 20 行比 20 个复制粘贴的测试块更易维护。
  • ✓测试代码用和生产代码一样的质量标准——抽 helper、避免重复、lint 它;腐烂的测试套件侵蚀整个套件的信心。