0Pricing
PHP Academy · レッスン

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フィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. テスト駆動開発のワークフロー
  2. Mockeryによるモックとスタブ
  3. 統合テストと機能テスト
  4. Infectionによるミューテーションテスト
← PHP Academyに戻る