编写测试应该激发人们对代码正确操作的信心。 通常,我们以代码覆盖的程度进行操作,当达到100%时,我们可以说解决方案是正确的。 您确定吗? 也许有一种工具可以提供更准确的反馈?
变异测试
这个术语描述了一种情况,在这种情况下,我们修改了一小段代码,并查看其如何影响测试。 在更改之后,如果测试正确执行,则表明对于这些代码段,测试还不够。 当然,这完全取决于我们要进行的更改,因为我们不需要测试所有最小的更改,例如缩进或变量名,因为在它们之后也必须正确完成测试。 因此,在变异测试中,我们使用所谓的变异器(修饰符方法),用一种有意义的方式将一段代码替换为另一段代码。 我们将在下面详细讨论。 有时我们自己进行这些测试,检查如果我们更改了代码中的某些内容,测试是否中断。 如果我们重构了“一半的系统”并且测试仍然是绿色的,那么我们可以立即说它们是不好的。 如果有人这样做并且测试很好,那么恭喜您!
传染性框架
如今,在PHP中,最流行的变异测试框架是
Infection 。 它支持PHPUnit和PHPSpec,要使用它,需要PHP 7.1+和Xdebug或phpdbg。
首次启动和配置
最初,我们会看到框架的交互式配置器,该配置器将创建一个特殊文件,其设置为-fection.json.dist。 看起来像这样:
{ "timeout": 10, "source": { "directories": [ "src" }, "logs": { "text": "infection.log", "perMutator": "per-mutator.md" }, "mutators": { "@default": true }
Timeout
-一个选项,其值应等于一个测试的最大持续时间。 在
source
我们指定将要对代码进行突变的目录,您可以设置例外。 在
logs
有一个
text
选项,我们将其设置为仅收集有关错误测试的统计信息,这对我们来说是最有趣的。
perMutator
选项允许您保存使用的
perMutator
器。 在文档中阅读有关此内容的更多信息。
例子
final class Calculator { public function add(int $a, int $b): int { return $a + $b; } }
假设我们有以上课程。 让我们用PHPUnit编写一个测试:
final class CalculatorTest extends TestCase { private $calculator; public function setUp(): void { $this->calculator = new Calculator(); } public function testAdd(int $a, int $b, int $expected): void { $this->assertEquals($expected, $this->calculator->add($a, $b)); } public function additionProvider(): array { return [ [0, 0, 0], [6, 4, 10], [-1, -2, -3], [-2, 2, 0] ]; } }
当然,在实现
add()
方法之前,需要编写此测试。 当执行
./vendor/bin/phpunit
我们得到:
PHPUnit 8.2.2 by Sebastian Bergmann and contributors. .... 4 / 4 (100%) Time: 39 ms, Memory: 4.00 MB OK (4 tests, 4 assertions)
现在运行
./vendor/bin/infection
:
You are running Infection with Xdebug enabled. ____ ____ __ _ / _/___ / __/__ _____/ /_(_)___ ____ /
根据感染,我们的测试是准确的。 在
per-mutator.md文件中
,我们可以看到使用了哪些突变:
Mutator Plus是符号从正到负的简单更改,应该会破坏测试。 而且,
PublicVisibility
mutator更改了此方法的访问修饰符,这也将破坏测试,在这种情况下它可以工作。
现在,让我们添加一个更复杂的方法。
public function findGreaterThan(array $numbers, int $threshold): array { return \array_values(\array_filter($numbers, static function (int $number) use ($threshold) { return $number > $threshold; })); } public function testFindGreaterThan(array $numbers, int $threshold, array $expected): void { $this->assertEquals($expected, $this->calculator->findGreaterThan($numbers, $threshold)); } public function findGreaterThanProvider(): array { return [ [[1, 2, 3], -1, [1, 2, 3]], [[-2, -3, -4], 0, []] ]; }
执行后,我们将看到以下结果:
You are running Infection with Xdebug enabled. ____ ____ __ _ / _/___ / __/__ _____/ /_(_)___ ____ /
我们的测试并不正确。 首先,检查infection.log文件:
Escaped mutants: ================ 1) /home/sarven/projects/infection-playground/infection-playground/src/Calculator.php:19 [M] UnwrapArrayValues --- Original +++ New @@ @@ */ public function findGreaterThan(array $numbers, int $threshold) : array { - return \array_values(\array_filter($numbers, static function (int $number) use($threshold) { + return \array_filter($numbers, static function (int $number) use($threshold) { return $number > $threshold; - })); + }); } 2) /home/sarven/projects/infection-playground/infection-playground/src/Calculator.php:20 [M] GreaterThan --- Original +++ New @@ @@ public function findGreaterThan(array $numbers, int $threshold) : array { return \array_values(\array_filter($numbers, static function (int $number) use($threshold) { - return $number > $threshold; + return $number >= $threshold; })); } Timed Out mutants: ================== Not Covered mutants: ====================
第一个未解决的问题是使用
array_values
函数。 它用于重置键,因为
array_filter
返回带有前一个数组的键的值。 另外,在我们的测试中,没有必要使用
array_values
,因为否则将返回具有相同值但键不同的数组。
第二个问题与边界案件有关。 相比之下,我们使用了
>
符号,但是我们不测试任何边界情况,因此用
>=
替换不会破坏测试。 您只需要添加一个测试:
public function findGreaterThanProvider(): array { return [ [[1, 2, 3], -1, [1, 2, 3]], [[-2, -3, -4], 0, []], [[4, 5, 6], 4, [5, 6]] ]; }
现在,感染使一切都感到满意:
You are running Infection with Xdebug enabled. ____ ____ __ _ / _/___ / __/__ _____/ /_(_)___ ____ /
向
Calculator
类添加一个
subtract
,但是在PHPUnit中没有单独的测试:
public function subtract(int $a, int $b): int { return $a - $b; }
运行感染后,我们看到:
You are running Infection with Xdebug enabled. ____ ____ __ _ / _/___ / __/__ _____/ /_(_)___ ____ /
这次,该工具返回了两个未发现的突变。
Escaped mutants: ================ Timed Out mutants: ================== Not Covered mutants: ==================== 1) /home/sarven/projects/infection-playground/infection-playground/src/Calculator.php:24 [M] PublicVisibility --- Original +++ New @@ @@ return $number > $threshold; })); } - public function subtract(int $a, int $b) : int + protected function subtract(int $a, int $b) : int { return $a - $b; } 2) /home/sarven/projects/infection-playground/infection-playground/src/Calculator.php:26 [M] Minus --- Original +++ New @@ @@ public function subtract(int $a, int $b) : int { - return $a - $b; + return $a + $b; }
指标
每次执行后,该工具都会返回三个指标:
Metrics: Mutation Score Indicator (MSI): 47% Mutation Code Coverage: 67% Covered Code MSI: 70%
Mutation Score Indicator
-测试检测到的突变百分比。
度量标准计算如下:
TotalDefeatedMutants = KilledCount + TimedOutCount + ErrorCount; MSI = (TotalDefeatedMutants / TotalMutantsCount) * 100;
Mutation Code Coverage
-
Mutation Code Coverage
的代码比例。
度量标准计算如下:
TotalCoveredByTestsMutants = TotalMutantsCount - NotCoveredByTestsCount; CoveredRate = (TotalCoveredByTestsMutants / TotalMutantsCount) * 100;
Covered Code Mutation Score Indicator
-仅针对测试
Covered Code Mutation Score Indicator
确定测试的有效性。
度量标准计算如下:
TotalCoveredByTestsMutants = TotalMutantsCount - NotCoveredByTestsCount; TotalDefeatedMutants = KilledCount + TimedOutCount + ErrorCount; CoveredCodeMSI = (TotalDefeatedMutants / TotalCoveredByTestsMutants) * 100;
在更复杂的项目中使用
在上面的示例中,只有一个类,因此我们运行了不带参数的感染。 但是在普通项目的日常工作中,使用
–filter
参数将很有用,该参数可让您指定要对其应用突变的文件集。
./vendor/bin/infection --filter=Calculator.php
误报
某些突变不会影响代码性能,并且感染会返回低于100%的MSI。 但是我们不能总是为此做些什么,因此我们必须适应这种情况。 此示例中显示了类似的内容:
public function calcNumber(int $a): int { return $a / $this->getRatio(); } private function getRatio(): int { return 1; }
当然,这里的
getRatio
方法
getRatio
意义,在正常项目中,可能会有某种计算代替。 但是结果可能是
1
。 感染返回:
Escaped mutants: ================ 1) /home/sarven/projects/infection-playground/infection-playground/src/Calculator.php:26 [M] Division --- Original +++ New @@ @@ public function calcNumber(int $a) : int { - return $a / $this->getRatio(); + return $a * $this->getRatio(); } private function getRatio() : int
众所周知,乘以1所得的结果相同,等于原始数。 因此,这种突变不应破坏测试,并且尽管对感染的准确性进行了剖析,但一切都井井有条。
大型项目的优化
在大型项目中,感染可能会花费很多时间。 如果仅处理修改过的文件,则可以在CI期间优化执行。有关更多信息,请参见文档:
https :
//infection.imtqy.com/guide/how-to.html此外,您可以在修改后的代码上并行运行测试。 但是,只有在所有测试都是独立的情况下才有可能。 即,这应该是很好的测试。 要启用此选项,请使用
–threads
:
./vendor/bin/infection --threads=4
如何运作?
感染框架使用AST(抽象语法树),它将代码表示为抽象数据结构。 为此,使用了由PHP的创建者之一(
php-parser )编写
的解析器 。
该工具的简化操作可以表示如下:
- 基于代码的AST生成。
- 使用合适的突变(完整列表在此处 )。
- 创建基于AST的修改后的代码。
- 针对更改后的代码运行测试。
例如,您可以检查减号替代变量加号:
<?php declare(strict_types=1); namespace Infection\Mutator\Arithmetic; use Infection\Mutator\Util\Mutator; use PhpParser\Node; use PhpParser\Node\Expr\Array_; final class Plus extends Mutator { public function mutate(Node $node) { return new Node\Expr\BinaryOp\Minus($node->left, $node->right, $node->getAttributes()); } protected function mutatesNode(Node $node): bool { if (!($node instanceof Node\Expr\BinaryOp\Plus)) { return false; } if ($node->left instanceof Array_ || $node->right instanceof Array_) { return false; } return true; } }
mutate()
方法创建一个新元素,并用加号替换。
Node
类取自php-parser包;用于AST操作和修改PHP代码。 但是,此更改无法应用于任何地方,因此
mutatesNode()
方法包含其他条件。 如果数组在加号的左侧或减号的右侧,则更改是不可接受的。 使用此条件的原因是以下代码:
$tab = [0] + [1]; is correct, but the following one isn't correct. $tab = [0] - [1];
总结
变异测试是一种出色的工具,可补充CI流程,并允许您评估测试的质量。 测试的绿色突出显示无法使我们确信所有内容都编写正确。 您可以使用突变测试(或测试测试)来提高测试的准确性,从而提高我们对解决方案性能的信心。 当然,不必总是争取100%的指标结果,因为这并不总是可能的。 您需要分析日志并相应地配置测试。