你的测试在撒谎吗?是时候进行突变测试了
\n\n你编写了单元测试,代码覆盖率(code coverage)看起来赏心悦目——85%,90%,但Bug仍然在生产环境中出现?这很常见。经典的覆盖率指标(行/分支覆盖率)本质上只是衡量哪些代码行被执行了。但它完全没有说明测试对逻辑的验证有多好。它们可能只是“跑过”了这些行,却没有检查结果的正确性。
\n\n这时,mutation testing(突变测试)就登场了。这就像给代码打了一针疫苗:我们故意在程序中引入微小的“病毒”(突变),然后观察我们的测试是否能杀死它们。如果测试在将===改为!==后没有失败——那你的测试就很薄弱。在pythonlib.ru的这篇文章中,我们将探讨如何将突变测试引入JavaScript项目,让你的代码真正可靠。
用简单的话来说什么是突变测试
\n\n想象一下,你是一名QA工程师,偷偷地修改了开发人员代码中的一行:
\n\n// 原来是\nif (user.age >= 18) { allowAccess(); }\n\n// 变成(突变)\nif (user.age > 18) { allowAccess(); }\n\n如果在这个替换之后,单元测试仍然全部通过——那么测试就没有检查边界条件18。突变测试框架会自动进行成千上万次这样的替换,并生成一份报告:有多少突变存活了下来(survived)。存活的突变越少,你的测试就越可靠。
JavaScript中主要的突变类型:
\n- \n
- 改变比较运算符:
>→>=,===→!==\n - 删除函数调用:
validate()→ 空字符串 \n - 改变数字字面量:
0→1,100→0\n - 反转逻辑条件:
if (isActive)→if (!isActive)\n - 替换字符串值(较少见,但也会发生) \n
为什么代码覆盖率是虚假的安全感
\n\n许多JavaScript开发者会被100%的代码覆盖率所迷惑。让我们看一个真实的例子:
\n\n// 一个计算折扣的函数\nfunction getDiscount(price, isPremium) {\n if (price > 100 && isPremium) {\n return price * 0.8;\n }\n return price;\n}\n\n以及“完美”的测试:
\n\ntest('should return price for non-premium', () => {\n expect(getDiscount(50, false)).toBe(50);\n});\n\ntest('should return price for premium with low price', () => {\n expect(getDiscount(50, true)).toBe(50);\n});\n\n覆盖率: 100%的行,100%的分支。但是突变 price > 100 → price >= 100 会存活下来!测试没有检查边界值100.01或正好100。突变测试会立即突出显示这个“盲点”。
工具一号:用于JavaScript的Stryker Mutator
\n\n在JS世界中最流行且最成熟的突变测试工具是Stryker Mutator(或简称为Stryker)。它支持Jest、Mocha、Jasmine、Karma,并且可以与TypeScript一起使用。
\n\n安装与快速入门
\n\n全局安装Stryker,或将其本地安装到你的项目中:
\n\nnpm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner\n\n创建一个配置文件stryker.conf.json:
{\n "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",\n "packageManager": "npm",\n "reporters": ["html", "clear-text", "progress"],\n "testRunner": "jest",\n "coverageAnalysis": "perTest",\n "thresholds": {\n "high": 80,\n "low": 60,\n "break": 50\n }\n}\n\n启动突变