反模式与最佳实践
基于 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 覆盖率(新增/改动代码的覆盖率),比整体数字更能反映本次改动质量