2024 年 3 月,我接手一个 Node.js 项目,Jest 测试 847 个,跑了 4 分 12 秒,覆盖率 93.7%。CI 每次 11 分钟,团队怨声载道。我一开始以为测试多是好事,结果发现 200 多个测试是重复的,还有 60 个在测 console.log 调用。改一个工具函数,红了 47 个测试,花了 2 小时改测试,代码只改了 3 行。这就是“测试负债”。
后来我们迁移到 Vitest 1.2.0,同样 847 个测试,时间降到 1 分 08 秒。为什么?Vitest 用 Vite 的 esbuild 转译,Jest 用 babel-jest。具体配置:test: { pool: 'threads', poolOptions: { threads: { singleThread: false, maxThreads: 8, minThreads: 2 } } }。但 Vitest 的 vi.mock 和 Jest 的 jest.mock 行为有差异,比如对 ESM 模块的 hoisting 处理,我们踩了 3 个坑,其中一个导致 mock 失效,测试假通过。所以不是无脑换。
真实问题:覆盖率陷阱。我曾经迷信 100% 覆盖率,直到发现一个 switch 的 default 分支永远走不到,但覆盖率算进去了。用 jest --coverage --collectCoverageFrom='src/**/*.js' 发现 branch 覆盖率只有 71%,而 line 是 94%。具体步骤:1. 运行 npx jest --coverage;2. 打开 coverage/lcov-report/index.html;3. 找到红色分支,手动补测试。但注意:不要为了覆盖率写 expect(true).toBe(true)。我们定了个规矩:核心模块 branch 覆盖率必须 >85%,工具函数 >70%,UI 组件 >60% 即可。
独立观点:单元测试应该测“契约”而不是“实现”。举个例子,一个 calculateDiscount(price, userLevel) 函数。以前我测内部调用了 applyVipDiscount,用了 jest.spyOn,结果重构把函数拆了,测试全挂。后来改成只测输入输出:expect(calculateDiscount(100, 'vip')).toBe(80),再加边界 0、负数、null。这样重构随便改,只要行为不变,测试就不动。这就是“行为测试”。另外,不要 mock 一切。我们一个测试 mock 了 7 个模块,后来发现真实数据库查询慢了 200ms,但 mock 完全没暴露。所以现在集成测试也做,单元测试只 mock 外部 HTTP 和随机数。
提速技巧。具体数字:jest --maxWorkers=50% 在 8 核机器上从 4 分 12 秒降到 2 分 58 秒。--changedSince=origin/main 只跑改动文件相关的测试,从 847 个降到 120 个,时间 22 秒。pytest -x --ff 快速失败,第一次跑全量,之后只跑失败的。还有 --silent 减少日志输出。最后,我现在的做法:单元测试不追求数量,追求“改了代码,如果测试没红,我就心慌;如果红了 10 个,说明测试太脆”。测试应该是安全网,不是紧身衣。