Infectionによるミューテーションテスト
テストが実際にどれほど優れているかを測定します。
「Infectionによるミューテーションテスト」はCoddyKit上の無料PHP Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応の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によって動作します。ミューテーションの対象にするディレクトリ、ログの出力先、CIを通過するための最低スコアしきい値を宣言します。source.directoriesは本番コードだけを指すようにしてください。テストを指定してはいけません。
{
"source": {
"directories": ["src"]
},
"logs": {
"text": "build/infection.log",
"html": "build/infection.html"
},
"mutators": {
"@default": true
},
"minMsi": 80,
"minCoveredMsi": 90
}MSIメトリクス
InfectionはMutation Score Indicatorをレポートします。
- MSI = キルされたミュータント数 / ミュータント総数。カバレッジのないコード(キルできないミュータント)によって低下します。
- Covered MSI = キルされたミュータント数 / カバレッジのある行のミュータント数。テストしている範囲で、アサーションがどれだけ適切かを測定します。
- Mutation Code Coverage = Infectionが実際にミューテーションできたコードの割合。
行カバレッジが高いのにCovered MSIが低い場合、アサーションが不足したテストであることを示す典型的なサインです。
Infectionを効率的に実行する
ミューテーションテストは、ミュータントごとにスイートを1回再実行するため高コストです。大きな高速化を2つ紹介します。--threadsでテストを並列実行すること、そしてGitの差分フィルタリングを使って現在のブランチで変更されたコードだけをミューテーションすることです。後者は、プルリクエストのCIに適しています。
vendor/bin/infection --threads=max --git-diff-lines --git-diff-base=origin/main生き残ったミュータントを読む
価値があるのは、Infectionが生き残った各ミュータントについて出力する差分です。テストが検出できなかった変更と、その正確な行を示します。生き残ったミュータントをToDoリストとして扱い、不足しているアサーションを追加するか、そのミュータントが無害(同値ミュータント)だと判断してください。
- 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"]
}
}
}ミューテーションテストが効果を発揮する場所
低速であるため、対象を適切に絞ってください。
- バグが見逃された場合のコストが高いコアドメインロジック(料金計算、権限、計算処理など)に適用します。
- リポジトリ全体ではなく、変更された行のCovered MSIを基準にPRをゲートします。
- 全体で100%を追い求めないでください。限界効用の低下や同値ミュータントのため、非効率になります。
- これを使って弱いアサーションを見つけ、テストを修正します。スコアは手段であり、目的ではありません。
高速化にはカバレッジデータが必要
Infectionは、テストが実際にカバーしている行だけをミューテーションするため、テストランナーのカバレッジを再利用します。Xdebugでは低速ですが、pcovは行カバレッジの取得が大幅に高速で、ミューテーション実行に推奨されるドライバーです。Infectionはカバレッジを自分で生成することも、CIですでに生成したカバレッジレポートを利用することもできます。
# 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簡単な確認
生き残ったミュータントは何を示していますか。
まとめ
テストの量だけでなく、テストの品質を測定する方法を学びました。
- ミューテーションテストは小さな障害(ミュータント)を注入します。キルされた場合は検出できたこと、生き残った場合は不足があることを示します。
- カバレッジは実行されたことを示し、Covered MSIはアサーションの強さを示します。
infection.json5でMSIのしきい値を設定し、高速化のために--threadsとGit差分フィルタリングを使って実行します。- 生き残ったミュータントをToDoリストとして扱い、同値ミュータントに注意して意図的に無視します。
- コアロジックを対象にし、全体で100%を追い求めるのではなく、変更行のMSIを基準にPRをゲートします。
よくある質問
「Infectionによるミューテーションテスト」レッスンは無料ですか?
はい。「Infectionによるミューテーションテスト」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、PHP Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 PHP Academyコースには全4レッスンが含まれています。
「Infectionによるミューテーションテスト」で何を学びますか?
テストが実際にどれほど優れているかを測定します。 ブラウザで直接実行するハンズオンコードでPHP Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
PHP Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのPHP Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。
「Infectionによるミューテーションテスト」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このPHP Academyレッスンでコードを書いて実行できますか?
はい。すべてのPHP Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- テスト駆動開発のワークフロー
- Mockeryによるモックとスタブ
- 統合テストと機能テスト
- Infectionによるミューテーションテスト