跳转到主要内容

测试

测试的目标是提高变更信心:覆盖关键路径、避免脆弱测试,并让失败信息可行动。

好的测试体系像安全网:能快速兜住回归,但不会拖慢开发。前提是你对“测什么”和“边界怎么拆”有纪律。

先测什么

按影响与频率排序。最有价值的测试,是保护用户每天都会走的路径。

  • 关键用户链路(登录、下单、保存等)。
  • `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 - React 文档 - React 文档