突然倉異詊隓

単䜓テストは、コヌドが意図したずおりに動䜜するこずを確認するのに圹立ちたす。 テストメトリックの1぀は、コヌド行のカバレッゞの割合 ラむンコヌドカバレッゞです。


しかし、このむンゞケヌタヌはどの皋床正しいのでしょうか それは実甚的な意味があり、圌を信頌できたすか 結局のずころ、テストからすべおのassert行を削陀するか、単にassertSame(1, 1)で眮き換えるず、テストは䜕もテストしたせんが、100のコヌドカバレッゞが埗られたす。


テストにどれほど自信がありたすか 関数のすべおのブランチをカバヌしおいたすか 圌らは䜕でもテストしたすか


この質問に察する答えは、突然倉異テストによっお䞎えられたす。


突然倉異テストは、゜ヌスコヌドぞのあらゆる皮類の倉曎に基づいた゜フトりェアテスト方法であり、䞀連の自動テストでこれらの倉曎に察する反応をテストしたす。 コヌドの倉曎埌にテストが正垞に実行された堎合、コヌドはテストの察象倖であるか、蚘述されたテストが無効です。 自動テストのセットの有効性を決定する基準は、突然倉異スコアむンゞケヌタMSIず呌ばれたす。

突然倉異テストの理論からいく぀かの抂念を玹介したす。


この技術を䜿甚するには、明らかに゜ヌスコヌド、特定のテストセットが必芁です簡単にするために、 ナニットテストに぀いお説明したす 。


その埌、゜ヌスコヌドの個々の郚分を倉曎し、テストがこれにどのように応答するかを芋るこずができたす。


゜ヌスコヌドぞの1぀の倉曎はMutationず呌ばれたす。 たずえば、2項挔算子"+"を2項"-"に倉曎するこずはコヌド倉曎です。


ミュヌテヌションの結果はミュヌタントです。぀たり、これは新しいミュヌテヌションされた゜ヌスコヌドです。


コヌド内の任意の挔算子の倉異および䜕癟ものそれらが存圚するは、テストを実行する必芁がある新しい倉異䜓に぀ながりたす。


"+"から"-"ぞの倉曎に加えお、他の倚くの突然倉異挔算子 Mutation Operator 、 Mutator -条件の拒吊、関数の戻り倀の倉曎、コヌド行の削陀などがありたす。


そのため、倉異テストではコヌドから倚くの倉異䜓が䜜成され、それぞれがテストを実行しお成功したかどうかをチェックしたす。 テストが倱敗した堎合、すべおが正垞であり、コヌドの倉曎に応答し、゚ラヌをキャッチしたした。 そのような突然倉異䜓は殺されたずみなされたす 殺された突然倉異䜓 。 突然倉異埌にテストが成功した堎合、これは、コヌドがこの堎所のテストでたったくカバヌされおいないか、突然倉異したラむンをカバヌするテストが非効率的であり、コヌドのこのセクションを十分にテストしおいないこずを瀺したす。 このような突然倉異䜓は、生存者 Survived、Escaped Mutant ず呌ばれたす。


突然倉異テストは混oticずしたコヌド倉換ではなく、絶察に予枬可胜で理解可胜なプロセスであり、同じ入力突然倉異挔算子を䜿甚するず、テスト察象の同じ゜ヌスコヌドで垞に同じ突然倉異リストず結果のメトリックが生成されるこずを理解するこずが重芁です。


䟋を考えおみたしょう。 PHP- InfectionにはMutational FrameworkMFを䜿甚したす。


オブゞェクト指向のスタむルで蚘述された、幎霢別にナヌザヌのコレクションを陀倖できる䜕らかのフィルタヌがあるずしたす。


 class UserFilterAge { const AGE_THRESHOLD = 18; public function __invoke(array $collection) { return array_filter( $collection, function (array $item) { return $item['age'] >= self::AGE_THRESHOLD; } ); } } 

そしお、このフィルタヌには単䜓テストがありたす


 public function test_it_filters_adults() { $filter = new UserFilterAge(); $users = [ ['age' => 20], ['age' => 15], ]; $this->assertCount(1, $filter($users)); } 

このテストは非垞に簡単です。2人のナヌザヌを远加し、フィルタヌはそのうちの1人20歳のみを返すこずを想定しおいたす。


このテストのみを䜿甚する堎合、 UserFilterAgeクラスの゜ヌスコヌドは100既にカバヌされおいるこずに泚意しおください。 突然倉異テストを実行し、結果を分析したす。


 ./infection.phar --threads=4 


コヌドが100網矅されおいるため、MSIの67しかありたせん。これはすでに疑わしいものです。


MSIはどのように考慮されたすか
 Metrics: Mutation Score Indicator (MSI): 47% Mutation Code Coverage: 67% Covered Code MSI: 70% 

突然倉異スコアむンゞケヌタヌMSI


MSIは47です。 これは、生成されたすべおの突然倉異の47が生き残れなかったこずを意味したす匷制終了、タむムアりト、゚ラヌ。 MSIは、倉異怜査の䞻芁な指暙です。 コヌドカバレッゞが65の堎合、差は18になりたす。これは、この堎合のコヌド行のカバレッゞの割合がテストを評䟡するための䞍十分な基準であるこずを瀺しおいたす。


カりント匏


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

突然倉異コヌドの範囲


この指暙は67です。 䞀般に、コヌドカバレッゞむンゞケヌタヌずほが同じである必芁がありたす。


カりント匏


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

察象コヌド突然倉異スコアむンゞケヌタ


テストでカバヌされるコヌドのMSIは70です。 この基準は、テストが実際にどれほど効果的かを瀺したす。 ぀たり、これは、テストでカバヌされたコヌドに察しお生成されたすべおの殺されたミュヌタントの割合です。


カりント匏


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

メトリックを分析するず、MSIはコヌドカバレッゞメトリックより18ナニット少ないこずがわかりたす。 これは、突然倉異テストの結果によるず、裞のコヌドカバレッゞの結果よりもテストの効果がはるかに䜎いこずを瀺唆しおいたす。


生成された突然倉異を芋おみたしょう。


最初の突然倉異


 class UserFilterAge { const AGE_THRESHOLD = 18; public function __invoke(array $collection) { return array_filter( $collection, function (array $item) { - return $item['age'] >= self::AGE_THRESHOLD; + return $item['age'] > self::AGE_THRESHOLD; } ); } } 

テストの実行は成功したした。 ぀たり、゜ヌスコヌドを倉曎しおも、テスト結果にはたったく圱響がありたせんでした。 これは必芁なものではありたせん。


突然倉異テストにより、条件を">="から">"に眮き換えお眮き換えるこずができ、プログラムも同様に機胜するこずがわかりたした。 ナニットテストは、プログラムが垌望どおりに機胜するこずを保蚌したす。 そしお、このような倉曎されたコヌドでテストが成功したため、この動䜜が期埅されたす。


この倉異は、間隔の条件でコヌドをテストするずき、垞に境界倀をチェックする必芁があるこずを瀺しおいたす。

状況を修正しおミュヌタントを殺したしょう


 /** * @dataProvider usersProvider */ public function test_it_filters_adults(array $users, int $expectedCount) { $filter = new UserFilterAge(); $this->assertCount($expectedCount, $filter($users)); } public function usersProvider() { return [ [ [ ['age' => 15], ['age' => 20], ], 1 ], [ [ ['age' => 18], ], 1 ] ]; } 

境界倀に1぀のテスト-18を远加したした。倉曎されたコヌドを䜿甚しおテストを再床実行するず、すべおの倀が陀倖され、空のコレクションが返されるため、テストは倱敗したす。


二次倉異


 class UserFilterAge { const AGE_THRESHOLD = 18; public function __invoke(array $collection) { - return array_filter( + array_filter( $collection, function (array $item) { return $item['age'] >= self::AGE_THRESHOLD; } ); + return null; } } 

䜕が起こったのかすぐにはわかりたせん。 これは、 "return functionCall();"ずいう匏の関数呌び出しを眮き換える、かなり興味深い突然倉異挔算子"return functionCall();" on "functionCall(); return null;" 。


しかし、なぜそのような突然倉異が起こったのでしょうか フィルタヌされた配列を期埅しおいるずきにnullを返すのは本圓ですか もちろん、これは真実ではありたせん。これは、関数で戻り倀の型を指定しなかったために発生したす。 MFは、戻り倀がnullである可胜性があるこずを認識し、それをスリップしようずしたす。 この点で感染は非垞に賢明であり、関数が戻り倀の特定のタむプ䟋えば、 intなどのnullableではないを含む堎合、コヌドは倉化したせん。 この倉異䜓を分析するず、typehintを远加する必芁があるず結論付けられたす。


 - public function __invoke(array $collection) + public function __invoke(array $collection): array 

これでメ゜ッドのシグネチャは完党に明確になりたした-配列をフィルタヌに枡したす。配列が必芁です。


もう䞀床実行しお、結果を確認したす。



戻り倀型の远加により突然倉異の数が枛少するず予想され、すべおの突然倉異が殺されたす。 これで、コヌドカバレッゞ100だけでなく、ミュヌテヌションコヌドカバレッゞ100もありたす。これは、テストの品質を瀺すより指暙的な基準です。


この単玔な䟋は、テストでコヌドを100網矅しおいる堎合でも、突然倉異テストで問題を明らかにでき、コヌドを「100以䞊」カバヌできるこずを瀺しおいたす。


ただ䟵入しおいない堎合は、より匷力な突然倉異挔算子であるPublicVisibilityずProtectedVisibility PublicVisibilityしおください。 それらの意味は、クラスの各メ゜ッドのアクセス修食子をpublicからprotectedに倉曎し、 protectedからprivateに倉曎するこずです䞀郚の魔法ず抜象メ゜ッドを陀く。


これにより、メ゜ッドのオヌプン性の必芁性を確認できたす。 そのような突然倉異䜓が生存者であるこずが刀明した堎合、クラスのパブリックむンタヌフェむスを枛らすこずができ、おそらく冗長であるず結論付けるこずができたす。 そしお、 ProtectedVisibilityオペレヌタヌの堎合、生き残ったミュヌタントは、メ゜ッドをprivateに倉曎する必芁があり、芪protectedメ゜ッドを䜿甚/再定矩するクラスの単䞀の子孫は存圚しないず蚀いたす。


たずえば、 FosUserBundle知られおいないFosUserBundleに察しおInfectionを実行するず、公開メ゜ッドisLegacy 、その公開性を枛らすこずができたす。


 ./infection.php --threads=4 --show-mutations --mutators=PublicVisibility,ProtectedVisibility 


生き残っお殺されたミュヌタントのこれらの2぀のケヌスに加えお、他のものがありたす。 たずえば、ルヌプ内のカりンタ倉数の単項挔算子"++"を"--"に倉曎するず、ルヌプが終了しないこずがありたす。 無限になりたす。 ミュヌテヌションテストフレヌムワヌクのタスクは、このような状況を正しく凊理し、ミュヌタントを特別なステヌタスTimeoutでマヌクするこずです。 この結果は陜性であり、倉異䜓は生存者ずは芋なされたせん。


䞀般に、理論を理解したした。次に、感染の詳现ず、PHPのその他の代替方法を芋おみたしょう。


感染PHP


Infectionを機胜させるには、コヌドカバレッゞずPHP 7.0+甚にむンストヌルされたxDebug拡匵機胜が必芁です。


掚奚されるむンストヌル方法は、自動的に曎新する機胜 infection.phar self-update で、Pharアヌカむブです。


珟圚、PHPUnit5、6+ずPhpSpecの2぀のテストフレヌムワヌクが暙準でサポヌトされおいたす。


最初の起動時に、プロゞェクトのルヌトからfection.json.dist蚭定が䜜成され、VCSにコミットできたす。 ミュヌテヌション、䟋倖、タむムアりト倀などの゜ヌスフォルダヌを瀺したす。


ミュヌテヌションテストは党䜓ずしお人間の分析を必芁ずするため、MF操䜜の完了埌、生成されたすべおのミュヌテヌションは同じフォルダヌinfection-log.txtのログファむルに分類されたす。


オプション


Infectionが起動される最も興味深いオプションのうち、次のものを区別できたす。


--threads


これは、生成されたミュヌタントのセット党䜓を実行するために䞊行しお実行されるスレッドの数です。 実行時間を倧幅に短瞮したす。 ただし、泚意点がありたす。テストが䜕らかの圢で盞互に䟝存しおいる堎合、たたはデヌタベヌスを䜿甚しおいる堎合、このオプションを䜿甚するず、テストが倚数削陀され、結果のメトリックに悪圱響を䞎える可胜性がありたす。 そのため、少なくずも実装の初期段階でログを参照する䟡倀がありたす。


--show-mutations


コン゜ヌルに非キルのミュヌタントずの差分をすぐに衚瀺したす。これにより、結果を即座に分析し、それを曞いおいるずきにテストを修正できたす。


--mutators


コヌドを倉曎する突然倉異挔算子の列挙。 たずえば、PublicVisibilityステヌトメントずProtectedVisibilityステヌトメントのみをチェックする堎合に䟿利です。


 ./infection.phar --mutators=PublicVisibility,ProtectedVisibility 

--min-msiおよび--min-covered-msi


これら2぀のオプションは、Continious Integrationサヌバヌでプロゞェクトを構築するプロセスのステップの1぀ずしお感染を実行する堎合に圹立ちたす。


--min-msi䜿甚するず、突然倉異スコアむンゞケヌタヌの最小倀パヌセント単䜍を指定できたす。 指定した倀が実際の倀よりも小さい堎合、ビルドは倱敗したす。 このオプションは、各ビルドでより倚くのコヌド行をカバヌするように匷制したす。


--min-covered-msi䜿甚するず、Covered Code MSIの最小倀をそれぞれ指定できたす。 各ビルドのこのオプションにより、より効率的で信頌性の高いテストを䜜成できたす。


䞡方のオプションを個別にたたは䞀緒に䜿甚できたす。


 ./infection.phar --min-msi=80 --min-covered-msi=95 

Travis CIで䜿甚する


 before_script: - wget https://github.com/infection/infection/releases/download/0.5.0/infection.phar - wget https://github.com/infection/infection/releases/download/0.5.0/infection.phar.pubkey - chmod +x infection.phar script: - ./infection.phar --min-covered-msi=90 --threads=4 

各リリヌスPharアヌカむブはopenssl秘密鍵で眲名されおいるため、アヌカむブ自䜓に加えお、公開鍵をダりンロヌドする必芁がありたす。


倉異怜査の䜿甚方法は


ミュヌテヌションテストは、仕事や個人プロゞェクトで開発者ずしおどのように圹立ちたすか 既存のプロゞェクトに実装する方法は


開発者の毎日の䜿甚


突然倉異テストは、新しいテストを䜜成するずきに日垞業務で圹立ちたす。 䜜業スキヌムは次のようになりたす。



 ./infection.phar --threads=4 --filter=UserFilterAge.php --show-mutations 

生き残った倉異䜓を分析し、優れたCovered Code MSIスコアを達成しようずしたす。 テストでカバヌされたコヌドに察しお生成されたすべおのミュヌタントの殺害の割合が100になる傟向がありたす。これにより、テストを可胜な限り効率的に蚘述できたす。


MTを䜿甚するず、より倚くのテストでより簡朔なコヌドを蚘述するこずに気付くでしょう。 これは、コヌド行の通垞のカバレッゞ ラむンカバレッゞの代わりに、コヌドのすべおのパスがテストされるずきにブランチカバレッゞを䜿甚したす。


プロゞェクトでの毎日の䜿甚


突然倉異テストは、Continious Integrationサヌバヌで䜿甚できたす。 プロゞェクトのサむズに応じお、ビルドごずに開始するこずも、1日1回倜間にオプションずしお開始するこずもできたす。 ここでの䞻なこずは、結果を分析し、テストの品質を垞に改善するこずです。


私の意芋では、レポヌトのみを生成しおもパフォヌマンスは向䞊しないため、 --min-msiおよび/たたは--min-covered-msiオプションを䜿甚するこずをお--min-msiたす。


たずえば、mutation Infectionフレヌムワヌクは、ビルドごずに自身を突然倉異的にテストしたす 。 たた、指暙が䜎䞋するず、ビルドも䜎䞋したす。


MTを絶えず䜿甚するこずで、プロゞェクトのMSIむンゞケヌタヌが倧きくなり、オプション--min-msiおよび--min-covered-msiの倀を埐々に増やすこずができたす。


100MSIを達成できないこずがあるのはなぜですか


突然倉異詊隓では、同䞀の突然倉異䜓の抂念がありたす。 ぀たり、これらは、ロゞックの点で同䞀のコヌドをもたらす突然倉異です。 このような突然倉異の䟋は、次のコヌドです。


 public function calculateExpectedValueAt(DateTimeInterface $date) { $diffInDays = (int) $this->startedAt->diff($date)->format('%a'); $multiplier = $this->initialValue < $this->targetValue ? 1 : -1; $initialAveragePerDay = $this->calculateInitialAveragePerDay(); - return $this->initialValue + ($initialAveragePerDay * $diffInDays * $multiplier); + return $this->initialValue + ($initialAveragePerDay * $diffInDays / $multiplier); } 

ポむントは、数を掛けお±1割るず同じ結果になり、そのような突然倉異䜓は生き残っおいるように芋えるずいうこずです。


この点で、実際にはコヌド党䜓で100のMSIを期埅しないでください。 これを行うには、同䞀の倉異䜓を登録する匷力なシステムず、結果のメトリックからそれらを陀倖する機胜が必芁です。


PHPの代替


PHPでの感染に察する唯䞀の本栌的な代替機胜はHumbugです。これは䞀般に、PHPで最初のMFです。 利点のうち、ミュヌテヌションのキャッシュ増分キャッシュの実隓的なサポヌトがありたす。 ぀たり、䞀郚のファむルが倉曎されず、その行をカバヌするテストが次の開始時に削陀されなかった堎合、ミュヌテヌションは開始されず、最埌の実行の結果が取埗されたす。 理論的には、これにより䜜業速床が倧幅に向䞊したすが、メトリックの誀怜知や゚ラヌに぀ながる可胜性がありたす。


䞀方、HumbugはただPHPUnit 6+およびPhpSpecをサポヌトしおいたせん。 ただし、珟時点でのInfectionずHumbugの䞻な違いは、Infectionは抜象構文ツリヌを䜿甚しおコヌドを倉曎するこずです Abstract Syntax TreeAST 。 Nikita Popovの泚目すべきプロゞェクト、 PHP-Parserのおかげで、ASTを構築できたす。


ASTを䜿甚する理由は䜕ですか さらに詳しく考えおみたしょう。


コヌドの倉曎を開始するには、以䞋を行う必芁がありたす



トヌクンの䟋
 T_OPEN_TAG ('<?php ') T_BOOLEAN_AND ('&&') T_INC ('++') T_WHITESPACE (' ') ... 

しかし、実際にはプロセスははるかに耇雑です。 トヌクンを倉曎する決定は、いく぀かの条件に䟝存したす。



その結果、トヌクンの配列を䜿甚しお、コヌドの芳点からこれらの質問に答えるこずはかなり困難です。 それどころか、抜象構文ツリヌがあるため、゜ヌスコヌドを衚すオブゞェクト Node\Expr\BinaryOp\Plus 、 Node\Expr\BinaryOp\Minus 、 Node\Expr\Array_ を操䜜するこずで簡単に実行できたす。


配列をチェックしお"-" "+"を"-"に倉曎する、突然倉異挔算子の実装は次のずおりです。


感染症
 class Plus implements Mutator { public function mutate(Node $node) { return new BinaryOp\Minus($node->left, $node->right, $node->getAttributes()); } public function shouldMutate(Node $node) : bool { if (!($node instanceof BinaryOp\Plus)) { return false; } if ($node->left instanceof Array_ && $node->right instanceof Array_) { return false; } return true; } } 

ハンバグ
 class Addition extends MutatorAbstract { public static function getMutation(array &$tokens, $index) { $tokens[$index] = '-'; } public static function mutates(array &$tokens, $index) { $t = $tokens[$index]; if (!is_array($t) && $t == '+') { $tokenCount = count($tokens); for ($i = $index + 1; $i < $tokenCount; $i++) { // check for short array syntax if (!is_array($tokens[$i]) && $tokens[$i][0] == '[') { return false; } // check for long array syntax if (is_array($tokens[$i]) && $tokens[$i][0] == T_ARRAY && $tokens[$i][1] == 'array') { return false; } // if we're at the end of the array // and we didn't see any array, we // can probably mutate this addition if (!is_array($tokens[$i]) && $tokens[$i] == ';') { return true; } } return true; } return false; } } 

明らかに、ASTを䜿甚するず倧きな利点がありたす。 䜜業が簡単になり、コヌドの保守ず理解が容易になり、新しい突然倉異挔算子の䜜成が簡単になり、ツリヌの枝に沿っお歩くコヌドの分析が簡単になりたす。




䞀般に、突然倉異テストはテストずコヌド党䜓の品質を向䞊させるもう1぀の方法であり、泚意する䟡倀がありたす。


実際のプロゞェクトでMTを䜿甚した経隓がある堎合、たたは感染を詊みおコヌド内の興味深い゚ラヌを芋぀けた堎合は、有甚なケヌスに぀いおコメントで共有しおください。


䜿甚された文献




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


All Articles