变异测试:测试测试


编写测试应该激发人们对代码正确操作的信心。 通常,我们以代码覆盖的程度进行操作,当达到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 { /** * @var Calculator */ private $calculator; public function setUp(): void { $this->calculator = new Calculator(); } /** * @dataProvider additionProvider */ 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. ____ ____ __ _ / _/___ / __/__ _____/ /_(_)___ ____ / // __ \/ /_/ _ \/ ___/ __/ / __ \/ __ \ _/ // / / / __/ __/ /__/ /_/ / /_/ / / / / /___/_/ /_/_/ \___/\___/\__/_/\____/_/ /_/ Running initial test suite... PHPUnit version: 8.2.2 9 [============================] 1 sec Generate mutants... Processing source code files: 1/1Creating mutated files and processes: 0/2 Creating mutated files and processes: 2/2 .: killed, M: escaped, S: uncovered, E: fatal error, T: timed out .. (2 / 2) 2 mutations were generated: 2 mutants were killed 0 mutants were not covered by tests 0 covered mutants were not detected 0 errors were encountered 0 time outs were encountered Metrics: Mutation Score Indicator (MSI): 100% Mutation Code Coverage: 100% Covered Code MSI: 100% Please note that some mutants will inevitably be harmless (ie false positives). Time: 1s. Memory: 10.00MB 

根据感染,我们的测试是准确的。 在per-mutator.md文件中我们可以看到使用了哪些突变:

 # Effects per Mutator | Mutator | Mutations | Killed | Escaped | Errors | Timed Out | MSI | Covered MSI | | ------- | --------- | ------ | ------- |------- | --------- | --- | ----------- | | Plus | 1 | 1 | 0 | 0 | 0 | 100| 100| | PublicVisibility | 1 | 1 | 0 | 0 | 0 | 100| 100| 

Mutator Plus是符号从正到负的简单更改,应该会破坏测试。 而且, PublicVisibility mutator更改了此方法的访问修饰符,这也将破坏测试,在这种情况下它可以工作。

现在,让我们添加一个更复杂的方法。

 /** * @param int[] $numbers */ public function findGreaterThan(array $numbers, int $threshold): array { return \array_values(\array_filter($numbers, static function (int $number) use ($threshold) { return $number > $threshold; })); } /** * @dataProvider findGreaterThanProvider */ 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. ____ ____ __ _ / _/___ / __/__ _____/ /_(_)___ ____ / // __ \/ /_/ _ \/ ___/ __/ / __ \/ __ \ _/ // / / / __/ __/ /__/ /_/ / /_/ / / / / /___/_/ /_/_/ \___/\___/\__/_/\____/_/ /_/ Running initial test suite... PHPUnit version: 8.2.2 11 [============================] < 1 sec Generate mutants... Processing source code files: 1/1Creating mutated files and processes: 0/7 Creating mutated files and processes: 7/7 .: killed, M: escaped, S: uncovered, E: fatal error, T: timed out ..M..M. (7 / 7) 7 mutations were generated: 5 mutants were killed 0 mutants were not covered by tests 2 covered mutants were not detected 0 errors were encountered 0 time outs were encountered Metrics: Mutation Score Indicator (MSI): 71% Mutation Code Coverage: 100% Covered Code MSI: 71% Please note that some mutants will inevitably be harmless (ie false positives). Time: 1s. Memory: 10.00MB 

我们的测试并不正确。 首先,检查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. ____ ____ __ _ / _/___ / __/__ _____/ /_(_)___ ____ / // __ \/ /_/ _ \/ ___/ __/ / __ \/ __ \ _/ // / / / __/ __/ /__/ /_/ / /_/ / / / / /___/_/ /_/_/ \___/\___/\__/_/\____/_/ /_/ Running initial test suite... PHPUnit version: 8.2.2 12 [============================] < 1 sec Generate mutants... Processing source code files: 1/1Creating mutated files and processes: 0/7 Creating mutated files and processes: 7/7 .: killed, M: escaped, S: uncovered, E: fatal error, T: timed out ....... (7 / 7) 7 mutations were generated: 7 mutants were killed 0 mutants were not covered by tests 0 covered mutants were not detected 0 errors were encountered 0 time outs were encountered Metrics: Mutation Score Indicator (MSI): 100% Mutation Code Coverage: 100% Covered Code MSI: 100% Please note that some mutants will inevitably be harmless (ie false positives). Time: 1s. Memory: 10.00MB 

Calculator类添加一个subtract ,但是在PHPUnit中没有单独的测试:

 public function subtract(int $a, int $b): int { return $a - $b; } 

运行感染后,我们看到:

 You are running Infection with Xdebug enabled. ____ ____ __ _ / _/___ / __/__ _____/ /_(_)___ ____ / // __ \/ /_/ _ \/ ___/ __/ / __ \/ __ \ _/ // / / / __/ __/ /__/ /_/ / /_/ / / / / /___/_/ /_/_/ \___/\___/\__/_/\____/_/ /_/ Running initial test suite... PHPUnit version: 8.2.2 11 [============================] < 1 sec Generate mutants... Processing source code files: 1/1Creating mutated files and processes: 0/9 Creating mutated files and processes: 9/9 .: killed, M: escaped, S: uncovered, E: fatal error, T: timed out .......SS (9 / 9) 9 mutations were generated: 7 mutants were killed 2 mutants were not covered by tests 0 covered mutants were not detected 0 errors were encountered 0 time outs were encountered Metrics: Mutation Score Indicator (MSI): 77% Mutation Code Coverage: 77% Covered Code MSI: 100% Please note that some mutants will inevitably be harmless (ie false positives). Time: 1s. Memory: 10.00MB 

这次,该工具返回了两个未发现的突变。

 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 )编写的解析器

该工具的简化操作可以表示如下:

  1. 基于代码的AST生成。
  2. 使用合适的突变(完整列表在此处 )。
  3. 创建基于AST的修改后的代码。
  4. 针对更改后的代码运行测试。

例如,您可以检查减号替代变量加号:

 <?php declare(strict_types=1); namespace Infection\Mutator\Arithmetic; use Infection\Mutator\Util\Mutator; use PhpParser\Node; use PhpParser\Node\Expr\Array_; /** * @internal */ final class Plus extends Mutator { /** * Replaces "+" with "-" * @param Node&Node\Expr\BinaryOp\Plus $node * @return Node\Expr\BinaryOp\Minus */ 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%的指标结果,因为这并不总是可能的。 您需要分析日志并相应地配置测试。

Source: https://habr.com/ru/post/zh-CN457888/


All Articles