Skip to content

反模式与最佳实践

基于 Vitest v4.x / Jest v30.x 编写

速查

  • 覆盖率 ≠ 测试质量:代码被执行 ≠ 被断言,100% 覆盖率仍可能零有效断言
  • 不要盲目追 100%:边际收益递减,为凑数写测试是负资产
  • 合理排除:*.d.ts / index.ts / *.config.* / types/** / __mocks__/**
  • 把覆盖率当下限门禁而非 KPI:低于 N% 阻止合并,但不奖励为覆盖率而写的测试
  • 重点盯 Branches 和关键路径,用 it.each 覆盖多分支

覆盖率 ≠ 测试质量

最大误区:覆盖率高不代表测得好。代码「被执行」不等于「被验证」。

ts
// ❌ 覆盖率 100%,但毫无价值——没有断言
it("calls processPayment", () => {
  processPayment({ amount: 100, currency: "USD" });
  // 没有 expect!即使函数抛错也不会让测试失败
});

// ✅ 有意义的断言
it("rejects negative amount", () => {
  expect(() => processPayment({ amount: -1, currency: "USD" })).toThrow(
    "Amount must be positive",
  );
});

前者 Statements / Lines / Functions 全 100%,却是无效测试。覆盖率工具看不出有没有断言

盲目追 100% 的代价

  • 为达标编写无意义测试,维护成本高
  • 强测错误处理分支(如 catch (e) { logger.error(e) })收益极低
  • 测试套件膨胀,CI 时间翻倍
  • 建议分层:核心业务 85-95%,工具类 70-80%,UI 组件视复杂度定

合理排除

ts
// 不该计入覆盖率的文件
exclude: [
  "**/*.d.ts", // 纯类型,无运行时
  "**/*.stories.{ts,js}", // Storybook story
  "**/index.ts", // 纯重导出
  "**/*.config.{ts,js}", // 配置文件
  "**/types/**", // 类型定义
  "**/__mocks__/**", // Mock 文件本身
  "**/migrations/**", // 数据库迁移
];

排除要克制:排太多会让覆盖率虚高、失去意义;排太少会让无关文件拖低数字。原则是「排掉没有业务逻辑、无法/无需测试的文件」。

把覆盖率当下限门禁

❌ 错误思路:「我们的覆盖率目标是 80%」(当成 KPI 去冲)
✅ 正确思路:「低于 75% 时 CI 阻止合并,鼓励尽量提高」(当成安全网)
  • 覆盖率下降(有人删测试 / 加了未测代码)→ CI fail,立刻发现
  • 覆盖率提高 → 好事,但不为「为覆盖率而写的测试」鼓掌
  • 真正的质量门:Branches ≥ N% + 关键路径 100% + 有意义的断言
  • autoUpdate 让基线随改善自动抬升,避免「冲到目标后躺平回退」

关注分支覆盖的做法

  • 用参数化测试(it.each / test.each)一次覆盖多个边界分支
  • && / || 短路、可选链 ?.、空值合并 ?? 专门补测试
  • 定期打开 HTML 报告,看红色未覆盖分支集中在哪,优先补关键路径
  • 关注 diff 覆盖率(新增/改动代码的覆盖率),比整体数字更能反映本次改动质量