你确定你的测试真的有效吗?
\n\n想象一下:你的代码覆盖率达到95%,所有测试都是绿色的,CI/CD顺利通过。团队很满意,经理很放心。但有一天,生产环境中出现了一个bug。你打开代码,查看测试,然后意识到:它们只是检查函数是否返回一个数字,但没有检查它的值。或者更糟——测试编写得无论如何都会通过。
\n\n熟悉的情况吗?欢迎来到一个覆盖率只是一个数字,而不是质量保证的世界。这时,突变测试(mutation testing)登场了——这是一种真正让你的测试经受考验并证明其有效性的方法。
\n\n在本文中,我们将探讨如何使用Stryker在TypeScript项目中引入突变测试,以及为什么这可能会成为你新的最爱工具(或新的头痛来源)。
\n\n用简单的话来说,什么是突变测试?
\n\n突变测试是指你拿自己的代码,故意在其中引入微小的错误(突变),然后观察你的测试是否会失败。如果测试没有失败——说明你的测试方式不正确。如果测试失败了——你做得很好,但突变体被消灭了。
\n\n听起来像施虐?也许吧。但这很有效。想象一下,你是一名QA工程师,偷偷潜入代码,将===替换为==,将>替换为<,或者删除了一个函数调用。一个好的测试必须能发现这一点。一个糟糕的测试则会放过它。
在具有严格类型系统的TypeScript中,突变测试特别有用:它能捕获类型正确但逻辑错误的情况。
\n\n为什么普通的代码覆盖率是一种错觉?
\n\n假设你有一个函数:
\n\nfunction calculateDiscount(price: number, isPremium: boolean): number {\n if (isPremium) {\n return price * 0.8;\n }\n return price;\n}\n\n以及一个测试:
\n\ntest('returns number', () => {\n const result = calculateDiscount(100, true);\n expect(typeof result).toBe('number');\n});\n\n覆盖率?100%。测试?通过。但是,如果我们将price * 0.8替换为price * 0.5呢?测试仍然会通过——因为它只检查类型,而不检查值。这就是虚假的安全感。
突变测试会突出显示这样的突变,并告诉你:“嘿,你的测试没有注意到我改变了折扣!”。然后你就不得不补充一个正常的检查。
\n\nTypeScript中的突变测试如何工作:分步指南
\n\n整个过程可以分为三个步骤:
\n\n- \n
- 生成突变体——工具复制你的代码并一次引入一个更改。例如,将
&&改为||,或删除一个方法调用。 \n - 运行测试——针对每个突变体运行你的测试。 \n
- 分析结果——如果测试失败,突变体被“杀死”。如果没有,它“存活”了下来,这就是风险区域。 \n
主要指标是突变分数(被杀死的突变体的百分比)。100%是理想状态,但在实践中,80-90%已经非常好了。
\n\nTypeScript的第一工具:Stryker
\n\n在TypeScript和JavaScript的世界里,突变测试的王者是Stryker(StrykerJS)。它速度快,能够与Jest、Vitest、Mocha甚至浏览器测试一起使用。5分钟即可完成配置。
\n\n安装:
\n\nnpm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner\n\n创建配置文件stryker.conf.js:
// stryker.conf.js\nexport default {\n mutate: ['src/**/*.ts', '!src/**/*.spec.ts'], // 要突变的内容\n testRunner: 'jest',\n reporters: ['html', 'clear-text', 'progress'],\n jest: {\n projectType: 'custom',\n configFile: 'jest.config.ts',\n },\n concurrency: 4, // 并发处理的突变体数量