使用 Infection 进行变异测试
衡量测试的实际质量
使用 Infection 进行变异测试 是 CoddyKit 上的免费 PHP Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 PHP Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 PHP Academy 课程共包含 4 节课。
覆盖率会说谎
100% 的行覆盖率令人安心,但它只能证明测试执行过这段代码,并不能证明测试会捕获其中的错误。测试可以运行某一行,却没有进行任何有意义的断言。变异测试衡量的是真实效果:如果代码被悄悄破坏,测试是否会失败?Infection 是 PHP 领域用于此目的的标准工具。
composer require --dev infection/infection核心概念:变异体
Infection 会在测试已覆盖的代码中引入微小的缺陷,即变异体。它会将 > 改为 >=,将 + 改为 -,将 && 改为 ||,移除 return,等等。然后,它会针对每个变异体重新运行测试:
- 如果测试失败 → 变异体被杀死(很好——测试捕获了这个缺陷)。
- 如果所有测试都通过 → 变异体存活(不好——真实的错误可能会在这里被忽略)。
存活变异体实例
请考虑下面这个函数和一个较弱的测试。Infection 会将 >= 变异为 >。如果没有测试检查精确边界(金额等于阈值),这个变异体就会存活,从而暴露出一个未测试的边界情况。
<?php
function qualifiesForFreeShipping(float $total): bool
{
return $total >= 50.0; // Infection mutates >= to >
}
// Weak test only checks 100 and 10 -> never tests exactly 50.0
var_dump(qualifiesForFreeShipping(100.0)); // true
var_dump(qualifiesForFreeShipping(10.0)); // false
var_dump(qualifiesForFreeShipping(50.0)); // true <-- the boundary the mutant exposes
杀死变异体
添加边界断言后,>= 到 > 的变异体就会被杀死:在变异后的代码中,50.0 > 50.0 的结果是 false,因此测试会失败——这正是我们想要的结果。变异测试会直接告诉您缺少哪些断言。
<?php
use PHPUnit\Framework\TestCase;
final class ShippingTest extends TestCase
{
public function test_threshold_is_inclusive(): void
{
// Kills the >= -> > mutant
self::assertTrue(qualifiesForFreeShipping(50.0));
}
}
配置:infection.json
Infection 由 infection.json5 驱动。您需要声明要进行变异的目录、日志存放位置,以及持续集成通过所需的最低分数阈值。将 source.directories 指向生产代码 בלבד,不要指向测试代码。
{
"source": {
"directories": ["src"]
},
"logs": {
"text": "build/infection.log",
"html": "build/infection.html"
},
"mutators": {
"@default": true
},
"minMsi": 80,
"minCoveredMsi": 90
}MSI 指标
Infection 会报告变异分数指标:
- MSI = 被杀死的变异体数 / 变异体总数。未覆盖的代码会拉低该指标(因为其中的变异体无法被杀死)。
- 覆盖代码 MSI = 被杀死的变异体数 / 已覆盖行上的变异体数。它衡量的是您实际测试的代码中,断言的质量。
- 变异代码覆盖率 = Infection 实际能够进行变异的代码比例。
行覆盖率很高但覆盖代码 MSI 很低,是断言不足的测试的典型信号。
高效运行变异测试
变异测试成本很高——它会针对每个变异体重新运行一次测试套件。两种主要的提速方法是:使用 --threads 并行运行测试,以及通过 Git 差异过滤只对当前分支改动触及的代码进行变异,这非常适合在拉取请求的持续集成中使用。
vendor/bin/infection --threads=max --git-diff-lines --git-diff-base=origin/main阅读存活的变异体
价值在于 Infection 为每个存活的变异体输出的差异。它会显示测试未能检测到的确切行和变更。请把存活的变异体当作待办清单:要么补充缺失的断言,要么确认该变异体没有实际影响(即等价变异体)。
- return $total >= 50.0;
+ return $total > 50.0;
# Mutant survived: no test asserts the inclusive boundary (total === 50.0)等价变异体与忽略处理
有些变异体是等价的——它们改变了代码,却没有改变可观察的行为,因此任何测试都不可能杀死它们。这些变异体无法被杀死,会不公平地拉低 MSI。请在特定代码上抑制已知会产生误报的变异算子,而不要为了追逐它们而操纵测试。
{
"mutators": {
"@default": true,
"Plus": {
"ignore": ["App\\Math\\Statistics::variance"]
}
}
}变异测试最有价值的地方
由于变异测试速度较慢,应选择合适的目标:
- 应用于核心领域逻辑——定价、权限、计算等,因为这里的隐性错误代价高昂。
- 以发生变更的代码行的覆盖代码 MSI作为拉取请求的门禁,而不是检查整个代码库。
- 不要追求全局 100%;边际收益递减,加上等价变异体,会使这种做法得不偿失。
- 用它来发现薄弱的断言,然后修复测试——分数是手段,不是目标。
依靠覆盖率数据提速
Infection 只会对测试实际覆盖的代码行进行变异,因此它会复用测试运行器生成的覆盖率数据。使用 Xdebug 时速度很慢;pcov 生成行覆盖率的速度快得多,是运行变异测试时推荐的驱动程序。Infection 也可以自行生成覆盖率,或者使用您已经在持续集成中生成的覆盖率报告。
# Faster mutation runs: use pcov instead of Xdebug for coverage
php -d pcov.enabled=1 vendor/bin/infection --threads=max
# Or reuse coverage already generated by your PHPUnit step:
vendor/bin/infection --coverage=build/coverage --skip-initial-tests快速检查
存活的变异体说明了什么?
回顾
您已经学会了衡量测试的质量,而不仅仅是数量:
- 变异测试注入微小缺陷(变异体);被杀死表示缺陷被捕获,存活表示存在测试缺口。
- 覆盖率显示代码是否被执行;覆盖代码 MSI 显示断言的力度。
- 通过
infection.json5配置 MSI 阈值;使用--threads和 Git 差异过滤来提速。 - 把存活的变异体作为待办清单;注意等价变异体,并有意忽略它们。
- 针对核心逻辑,并以变更代码行的 MSI 作为拉取请求的门禁,而不是追求全局 100%。
常见问题解答
「使用 Infection 进行变异测试」课时是免费的吗?
是的 — 「使用 Infection 进行变异测试」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 PHP Academy 课程的其余内容,请升级到 CoddyKit PRO。 PHP Academy 课程共包含 4 节课。
「使用 Infection 进行变异测试」这节课中我会学到什么?
衡量测试的实际质量 你通过在浏览器中直接运行的动手代码来练习 PHP Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 PHP Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 PHP Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「使用 Infection 进行变异测试」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 PHP Academy 课中编写并运行代码吗?
能。每节 PHP Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 测试驱动开发工作流
- 使用 Mockery 进行模拟与存根
- 集成测试与功能测试
- 使用 Infection 进行变异测试