Mutationstests: Testtests


Das Schreiben von Tests sollte Vertrauen in die korrekte Funktionsweise des Codes schaffen. Oft arbeiten wir mit dem Grad der Abdeckung des Codes, und wenn wir 100% erreichen, können wir sagen, dass die Lösung korrekt ist. Bist du dir da sicher? Vielleicht gibt es ein Tool, das genaueres Feedback gibt?

Mutationstests


Dieser Begriff beschreibt eine Situation, in der wir kleine Codeteile ändern und sehen, wie sich dies auf die Tests auswirkt. Wenn die Tests nach den Änderungen korrekt ausgeführt werden, bedeutet dies, dass die Tests für diese Codeteile nicht ausreichen. Natürlich hängt alles davon ab, was genau wir ändern, da wir nicht alle kleinsten Änderungen testen müssen, z. B. Einrückungen oder Variablennamen, da danach die Tests auch korrekt abgeschlossen werden müssen. Daher verwenden wir in Mutationstests die sogenannten Mutatoren (Modifikatormethoden), die einen Code durch einen anderen ersetzen, aber auf eine sinnvolle Weise. Wir werden im Folgenden ausführlicher darauf eingehen. Manchmal führen wir solche Tests selbst durch und prüfen, ob die Tests nicht funktionieren, wenn wir etwas im Code ändern. Wenn wir „die Hälfte des Systems“ überarbeitet haben und die Tests immer noch grün sind, können wir sofort sagen, dass sie schlecht sind. Und wenn jemand dies getan hat und die Tests gut waren, dann herzlichen Glückwunsch!

Infektiöser Rahmen


Heute ist in PHP das beliebteste Framework für Mutationstests die Infektion . Es unterstützt PHPUnit und PHPSpec und um damit zu arbeiten, sind PHP 7.1+ und Xdebug oder phpdbg erforderlich.

Erster Start und Konfiguration


Beim ersten Start sehen wir den interaktiven Konfigurator des Frameworks, der eine spezielle Datei mit den Einstellungen infection.json.dist erstellt. Es sieht ungefähr so ​​aus:

{ "timeout": 10, "source": { "directories": [ "src" }, "logs": { "text": "infection.log", "perMutator": "per-mutator.md" }, "mutators": { "@default": true } 

Timeout - eine Option, deren Wert der maximalen Dauer eines Tests entsprechen sollte. In der source wir die Verzeichnisse an, aus denen wir den Code mutieren. Sie können Ausnahmen festlegen. In logs gibt es eine Textoption, mit der wir Statistiken nur zu fehlerhaften Tests erfassen, was für uns am interessantesten ist. Mit der Option perMutator können Sie gebrauchte Mutatoren speichern. Lesen Sie mehr dazu in der Dokumentation.

Beispiel


 final class Calculator { public function add(int $a, int $b): int { return $a + $b; } } 

Nehmen wir an, wir haben die obige Klasse. Schreiben wir einen Test in 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] ]; } } 

Natürlich muss dieser Test geschrieben werden, bevor wir die add() -Methode implementieren. Bei der Ausführung von ./vendor/bin/phpunit wir:

 PHPUnit 8.2.2 by Sebastian Bergmann and contributors. .... 4 / 4 (100%) Time: 39 ms, Memory: 4.00 MB OK (4 tests, 4 assertions) 

Führen ./vendor/bin/infection nun ./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 

Laut Infection sind unsere Tests genau. In der Datei per-mutator.md können wir sehen, welche Mutationen verwendet wurden:

 # 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 ist ein einfacher Vorzeichenwechsel von Plus nach Minus, der die Tests unterbrechen sollte. Der PublicVisibility Mutator ändert den Zugriffsmodifikator dieser Methode, wodurch auch die Tests PublicVisibility sollen. In diesem Fall funktioniert er.

Fügen wir nun eine kompliziertere Methode hinzu.

 /** * @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, []] ]; } 

Nach der Ausführung sehen wir das folgende Ergebnis:

 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 

Unsere Tests sind nicht in Ordnung. Überprüfen Sie zunächst die Datei instance.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: ==================== 

Das erste nicht array_values Problem ist die Verwendung der Funktion array_values . Es wird zum Zurücksetzen von Schlüsseln verwendet, da array_filter die Werte mit Schlüsseln aus dem vorherigen Array zurückgibt. Darüber hinaus gibt es in unseren Tests keinen Fall, in dem die Verwendung von array_values erforderlich ist, da ansonsten ein Array mit denselben Werten, aber unterschiedlichen Schlüsseln zurückgegeben wird.

Das zweite Problem betrifft Grenzfälle. Im Vergleich dazu haben wir das Zeichen > , aber wir testen keine Grenzfälle, sodass das Ersetzen durch >= die Tests nicht unterbricht. Sie müssen nur einen Test hinzufügen:

 public function findGreaterThanProvider(): array { return [ [[1, 2, 3], -1, [1, 2, 3]], [[-2, -3, -4], 0, []], [[4, 5, 6], 4, [5, 6]] ]; } 

Und jetzt ist Infection mit allem zufrieden:

 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 

Fügen Sie der Calculator Klasse eine subtract , jedoch ohne separaten Test in PHPUnit:

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

Und nach dem Ausführen von Infection sehen wir:

 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 

Dieses Mal gab das Tool zwei nicht abgedeckte Mutationen zurück.

 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; } 

Metriken


Nach jeder Ausführung gibt das Tool drei Metriken zurück:

 Metrics: Mutation Score Indicator (MSI): 47% Mutation Code Coverage: 67% Covered Code MSI: 70% 

Mutation Score Indicator - Der Prozentsatz der durch Tests erkannten Mutationen.

Die Metrik wird wie folgt berechnet:

 TotalDefeatedMutants = KilledCount + TimedOutCount + ErrorCount; MSI = (TotalDefeatedMutants / TotalMutantsCount) * 100; 

Mutation Code Coverage von Mutationscodes - Der Anteil des Codes, der von Mutationen abgedeckt wird.

Die Metrik wird wie folgt berechnet:

 TotalCoveredByTestsMutants = TotalMutantsCount - NotCoveredByTestsCount; CoveredRate = (TotalCoveredByTestsMutants / TotalMutantsCount) * 100; 

Covered Code Mutation Score Indicator - Bestimmt die Wirksamkeit von Tests nur für Code, der von Tests abgedeckt wird.

Die Metrik wird wie folgt berechnet:

 TotalCoveredByTestsMutants = TotalMutantsCount - NotCoveredByTestsCount; TotalDefeatedMutants = KilledCount + TimedOutCount + ErrorCount; CoveredCodeMSI = (TotalDefeatedMutants / TotalCoveredByTestsMutants) * 100; 

Verwendung in komplexeren Projekten


Im obigen Beispiel gibt es nur eine Klasse, daher haben wir Infection ohne Parameter ausgeführt. Bei der täglichen Arbeit an normalen Projekten ist es jedoch hilfreich, den Parameter –filter zu verwenden, mit dem Sie den Satz von Dateien angeben können, auf die Mutationen angewendet werden sollen.

 ./vendor/bin/infection --filter=Calculator.php 

False Positives


Einige Mutationen wirken sich nicht auf die Codeleistung aus, und Infection gibt einen MSI unter 100% zurück. Aber wir können nicht immer etwas damit anfangen, also müssen wir uns mit solchen Situationen auseinandersetzen. In diesem Beispiel wird etwas Ähnliches gezeigt:

 public function calcNumber(int $a): int { return $a / $this->getRatio(); } private function getRatio(): int { return 1; } 

Natürlich macht hier die getRatio Methode getRatio Sinn, in einem normalen Projekt würde es wahrscheinlich stattdessen eine Art Berechnung geben. Aber das Ergebnis könnte 1 . Infektion kehrt zurück:

 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 

Wie wir wissen, ergibt das Multiplizieren und Teilen mit 1 das gleiche Ergebnis, das der ursprünglichen Zahl entspricht. Diese Mutation sollte also die Tests nicht brechen, und trotz der Unzufriedenheit von Infection mit der Genauigkeit unserer Tests ist alles in Ordnung.

Optimierung für große Projekte


In Fällen mit großen Projekten kann die Infektion viel Zeit in Anspruch nehmen. Sie können die Ausführung während des CI optimieren, wenn Sie nur geänderte Dateien verarbeiten. Weitere Informationen finden Sie in der Dokumentation: https://infection.imtqy.com/guide/how-to.html

Darüber hinaus können Sie parallel Tests für den geänderten Code ausführen. Dies ist jedoch nur möglich, wenn alle Tests unabhängig sind. Das sollten nämlich gute Tests sein. Verwenden Sie die –threads um diese Option zu –threads :

 ./vendor/bin/infection --threads=4 

Wie funktioniert es


Das Infection-Framework verwendet AST (Abstract Syntax Tree), das Code als abstrakte Datenstruktur darstellt. Hierzu wird ein Parser verwendet, der von einem der Entwickler von PHP ( PHP-Parser ) geschrieben wurde.

Die vereinfachte Bedienung des Werkzeugs kann wie folgt dargestellt werden:

  1. Codebasierte AST-Generierung.
  2. Die Verwendung geeigneter Mutationen (die vollständige Liste finden Sie hier ).
  3. Erstellen Sie AST-basierten geänderten Code.
  4. Führen Sie Tests in Bezug auf den geänderten Code aus.

Sie können beispielsweise den Minus-Substitutionsmutator plus überprüfen:

 <?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; } } 

Die mutate() -Methode erstellt ein neues Element, das durch ein Plus ersetzt wird. Die Node Klasse stammt aus dem PHP-Parser-Paket und wird für AST-Operationen und zum Ändern von PHP-Code verwendet. Diese Änderung kann jedoch nirgendwo angewendet werden, sodass die mutatesNode() -Methode zusätzliche Bedingungen enthält. Befindet sich ein Array links vom Plus oder rechts vom Minus, ist die Änderung nicht zulässig. Diese Bedingung wird aufgrund dieses Codes verwendet:

 $tab = [0] + [1]; is correct, but the following one isn't correct. $tab = [0] - [1]; 

Zusammenfassung


Mutationstests sind ein hervorragendes Tool, das den CI-Prozess ergänzt und es Ihnen ermöglicht, die Qualität von Tests zu bewerten. Die grüne Hervorhebung der Tests gibt uns kein Vertrauen, dass alles gut geschrieben ist. Sie können die Genauigkeit von Tests mithilfe von Mutationstests oder Testtests erhöhen, wodurch unser Vertrauen in die Leistung der Lösung erhöht wird. Natürlich ist es nicht notwendig, 100% ige Ergebnisse von Metriken anzustreben, da dies nicht immer möglich ist. Sie müssen die Protokolle analysieren und die Tests entsprechend konfigurieren.

Source: https://habr.com/ru/post/de457888/


All Articles