🐘

PHPのMutation Testingライブラリ・Infectionを実戦導入してみた

2024/04/10に公開

📢 少しだけ宣伝させてください 🙏

来る7/20にPHPカンファレンス神戸を開催します!!

https://blessingsoftware.connpass.com/event/315796/

https://speakerdeck.com/ysknsid25/php-ore-kanhuarensunogao-zhi

小さい箱での開催のため、参加枠に限りがあります!ぜひお早めにお申し込みください!

改めて、はじめに

こんにちは、今回も株式会社hitocolorのKanonとしてお目にかかます。

先日、『Mutation Testingライブラリ・Strykerを実戦導入してみた』という記事を公開しました。

https://zenn.dev/hitocolor/articles/3b6792cc9887df

今回はある意味、前回の続編。バックエンドへMutation Testを導入してみたので、同じく実戦導入にあたってのアレコレをお話ししたいなと思い筆を取りました。

おそらくInfectionを実戦導入例を踏まえての記事は、これが日本だと初めてではないでしょうか。

実はこれの続編でもある

この記事はMutaion Testの実戦導入においてフロント続編なのですが、実は以下の記事の続編でもあります。

https://zenn.dev/bs_kansai/articles/3a198f77e60d40

実は以前から(個人的に)Infectionを試験的には使っており、こちらはPHPカンファレンス関西2024でも発表させていただいていました。

https://fortee.jp/phpcon-kansai2024/proposal/8daa1c68-69b1-458a-9f3a-0c9a86e7843e

https://speakerdeck.com/ysknsid25/phpkanhuarensuguan-xi-2024

しかし、CI時にMutation Testを実行することを視野に入れる場合、以下の3点が課題として残っていました。

  • MSIのスコアが一定以下の場合、CIをエラーとする
  • CI時に毎回動くため、全てのファイルではなくGit上差分があるファイルだけを対象とする
  • 実行レポートを参照する

今回の実戦導入では上記3点の課題を解決できたため、そこにも触れながらお話しできればと思います。

また今回の実戦導入では全体に対するMutaion Testを定期実行PR時に差分に対してMutaion Testを実行の二つのゴールがあり、そこも踏まえつつお話ししたいと思います。

初期導入の流れ

composer require --dev infection/infection

導入後、vendor/bin/infectionを実行することで設定ファイルの作成と初回実行をしてくれます。

設定ファイル作成時に聞かれることは、

  • Mutate を発生させる対象ディレクトリ
  • Mutation Testing の対象から除外するディレクトリ
  • テキストログファイルをどこに保存するか

この 3 点です。今回は以下のように回答し設定ファイルが作成された後Infectionが実行されます。

しかし、やはり初期設定のまま無邪気に実行してみるとStrykerの時といろいろと問題にぶちあたりました。

課題

  • やはり無邪気に実行するととてつもなく遅い
  • CI時に都度実行するにあたっての課題
    • MSIのスコアが一定以下の場合、CIをエラーとする
    • CI時に毎回動くため、全てのファイルではなくGit上差分があるファイルだけを対象としたい
    • 実行レポートを参照したい

改善

ここからは観測できる範囲で直面した問題と、それにどう対処したかをお話しします。

やはり無邪気に実行するととてつもなく遅い問題

フロントで導入した際の知見が役にたちました。しかし、Strykerに比べてInfectionは高速に動作するため、そこまで絶望感ある実行速度ではありませんでした。

Strykerの時と同じような対処をとることで、定期実行によりテスト品質向上のため測定したいファイル全てを検証したときも、CI時に差分に対して実行したときも、それぞれ現実的な時間内に処理が終了するようになりました。

また--threads=max,--only-covered,--only-covering-test-casesオプションを付けることで、最大並列かつテストコードがカバーしているファイルに対してのみMutaion Testを実行するようにし、処理速度を向上させています。

変異発生対象を絞った

今回もやはり変異発生対象を絞っています。バックエンドはLaravelで動いているおり、今回対象としたのはService,Middleware,Controllerでした。

変異対象は設定ファイル(infection.json5)に記載することで絞り込みが可能です。

本来であればControllerは対象外でよいと考えています。しかし、悲しいかなFat Controllerになっている箇所があるため泣く泣く対象とせざるをえませんでした。

https://infection.github.io/guide/usage.html#Configuration-settings

"source": {
    "directories": [
        //変異の対象とするディレクトリ
    ],
    "excludes": [
        //変異対象外とするディレクトリ
    ]
},

変異の種類を絞った

次にやはり発生させる変異の種類を絞りました。以下のように記載することで、変異対象を間引いていくことが可能です。

間引く対象は以下のドキュメントの見出しの項目をキャメルケースで設定ファイル(infection.json5)に記載します。

https://infection.github.io/guide/mutators.html

"mutators": {
    "@default": true,
    "@cast": false,
    "@function_signature": false,
}

CI時はユニットテストに続けてMutation Testを実行する

InfectionはMutation Testの前にユニットテストを実行します。

https://infection.github.io/guide/command-line-options.html#skip-initial-tests

ですがCI時においては事前にユニットテストが実行されているため、Mutation Testの実行前にもわざわざ実行する必要がありません。

そのため、--skip-initial-testsを使ってInfection実行前のユニットテストの実行を省略します。

ただし、テストをスキップするためにはxmljunitの2種類のカバレッジレポートが提供されている必要があるため、以下のようなオプションをつけてPHPUnitを実行する必要があります。

XDEBUG_MODE=coverage php artisan test --coverage-xml=report/coverage/coverage-xml --log-junit=report/coverage/junit.xml

MSIのスコアが一定以下の場合、CIをエラーとする問題

--min-msiオプションを付けることで、解決できます。

https://infection.github.io/guide/command-line-options.html#min-msi

閾値を下回った場合、以下のようなエラーとともに終了します。

CI時に毎回動くため、全てのファイルではなくGit上差分があるファイルだけを対象としたい問題

全ファイルを対象にInfectionを実行した場合、導入環境だと40分弱かかっています。

CIの度に42分も待つのは現実的ではないため、やはりStrykerと同じく差分に対してのみ実行したいです。

そこで--git-diff-filter--git-diff-baseオプションを付けることで差分に対する実行が可能となります。

公式ドキュメントには使用例も載せてくれています。

https://infection.github.io/guide/command-line-options.html#git-diff-base

git fetch --depth=1 origin $GITHUB_BASE_REF
infection.phar --git-diff-base=origin/$GITHUB_BASE_REF --git-diff-filter=AM

ただし、ここで一つ要注意なポイントがあります。ユニットテスト+Mutaion Testのワークフローのトリガーがon.pushon.pull_requestになっている場合です。

$GITHUB_BASE_REFはGHAで事前定義されている環境変数なのですが、pull_requestまたはpull_request_target以外のトリガーで起動した場合は値が未定義となります。

そのため--git-diff-base=origin/としてブランチの比較が行われるため、当然比較対象のブランチが存在せずエラーとなります。

よって以下のように、pull_requestの場合にしか実行されないようにしてあげる必要があります。

- name: Run Mutation Test
  if: ${{ github.event_name == 'pull_request' }}
  run: |
    git fetch --depth=1 origin $GITHUB_BASE_REF

これで差分がある場合は以下のように実行され・・・

差分がない場合は以下のように実行されます。

実行レポートを参照したい問題

定期実行時は前回のStrykerと同じくGSCにsHTMLレポートをアップロード→静的ホスティング→SlackでURLを通知という流れで実行レポートを参照できるようにしました。

が、気がかりだったのがCI実行時のレポートです。流石に毎回GCSにファイルを上げるのは開発速度の低下につながりかねません。

しかしその悩みは杞憂で、この点Infectionは素晴らしい機能を提供してくださっていました。

--logger-github=trueを利用する

このオプションを付与することで、PRのfilesにGHAが「どの変異が生き残ったか?」をコメントしてくれます。

コレを見てレビュイーはソースコードを修正すればよいです。

今後の課題

こちらはフロントよりも後に導入したということもあり、かなりハマりポイントや完成形が見えた状態で作業できたためMutaion Test実行環境そのものに対する改善点などは今のところありません。

なので、ここからはテスト全体のMSIを上げていく(=生き残った変異を摘み取れる)ようにテストコードの品質を上げていく作業が必要だと考えています。

CI時にMSIが一定以下であればGHAがコケるようになっているため、これから修正&追加されるコードに関しては一定以上のMSIを保つことができるようになっています。

が、潜在的にMSIが閾値を下回っている箇所については修正が必要なため、全体実行されたレポートを見つつ、テストコードを修正する作業を新卒エンジニアさんにやっていっていただく予定です。

外部の人間から見て「この作業の振り方いいな…」と思っていて、テストコードを読みつつ仕様を掴めるし、簡単な修正がほとんどになってくるためハンズオンにも申し分ないなと関心を持っています。

やはりテストコードを書くメリットは大きい・・・

おわりに

PHPカンファレンス関西では理屈と簡単な導入だけで話が終わってしまいましたが、今回実戦導入した話とその後というところも含めて12月のPHPカンファレンスでお話ししたいなぁ・・・

株式会社hitocolor

Discussion