测试
测试的目标是提高变更信心:覆盖关键路径、避免脆弱测试,并让失败信息可行动。
好的测试体系像安全网:能快速兜住回归,但不会拖慢开发。前提是你对“测什么”和“边界怎么拆”有纪律。
先测什么
按影响与频率排序。最有价值的测试,是保护用户每天都会走的路径。
- 关键用户链路(登录、下单、保存等)。
- `error`/`loading`/`empty` 三态——最容易漏,也最影响体验。
- 逻辑边界(`reducer`、解析器、校验器)。
测试金字塔(务实版)
用“反馈速度”和“信心强度”来思考。覆盖面主要来自快速稳定的测试;慢测试只留给关键路径。
如果测试太慢,大家就不跑;如果测试不稳定,大家就不信。速度与确定性是“一等公民”。
- 单元测试:纯逻辑(reducer/解析/校验)。最快且最稳定。
- 集成测试:组件 + 边界(渲染、交互、网络 mock)。
- `E2E`:只测“绝不能坏”的流程。最慢也最贵。
测行为,不测实现
- 优先断言用户看到/做到的结果(文本、按钮可用性、导航结果),而不是内部 state 结构。
- 重构时行为应稳定;测试不应该成为重构的阻力。
可替换边界:时间、网络、随机性
脆弱测试常来自不可控的时间与网络。把边界做成可注入,测试就能变得可重复。
这也会反向促进设计:可测试的代码往往更易维护,因为依赖是显式的。
TypeScript
export type Clock = { now(): number };
export type Random = { next(): number };
export type Fetcher = (url: string) => Promise<{ ok: boolean; json(): Promise<any> }>;
export function createServices(deps: Partial<{ clock: Clock; random: Random; fetcher: Fetcher }> = {}) {
const clock: Clock = deps.clock ?? { now: () => Date.now() };
const random: Random = deps.random ?? { next: () => Math.random() };
const fetcher: Fetcher =
deps.fetcher ?? (async (url) => fetch(url));
return { clock, random, fetcher };
}
// Production
const services = createServices();
// Test (deterministic)
const testServices = createServices({
clock: { now: () => 1700000000000 },
random: { next: () => 0.42 },
fetcher: async () => ({ ok: true, json: async () => ({ items: [] }) }),
});常见坑
- 过度 mock 导致测试与真实行为脱节。
- 只测 happy path,忽略 `loading`/`error`/`empty`。
- 在 `CI` 中依赖真实定时器/真实网络,导致不稳定与反馈变慢。
上线检查清单
- 关键流程至少有 1 条稳定 E2E;其他由更快的测试覆盖。
- `loading`/`error`/`empty` 三态有覆盖(用户可感知,且容易回归)。
- `CI` 信号可行动:`lint`、类型检查、构建 + 小规模冒烟套件。
- 测试可复现:时间/网络/随机性已被控制。
延伸阅读
- Guides:升级