PHPのMutation Testingライブラリ・Infectionを実戦導入してみた
📢 少しだけ宣伝させてください 🙏
来る7/20にPHPカンファレンス神戸を開催します!!
小さい箱での開催のため、参加枠に限りがあります!ぜひお早めにお申し込みください!
改めて、はじめに
こんにちは、今回も株式会社hitocolorのKanonとしてお目にかかます。
先日、『Mutation Testingライブラリ・Strykerを実戦導入してみた』という記事を公開しました。
今回はある意味、前回の続編。バックエンドへMutation Testを導入してみたので、同じく実戦導入にあたってのアレコレをお話ししたいなと思い筆を取りました。
おそらくInfectionを実戦導入例を踏まえての記事は、これが日本だと初めてではないでしょうか。
実はこれの続編でもある
この記事はMutaion Testの実戦導入においてフロント続編なのですが、実は以下の記事の続編でもあります。
実は以前から(個人的に)Infectionを試験的には使っており、こちらはPHPカンファレンス関西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
になっている箇所があるため泣く泣く対象とせざるをえませんでした。
"source": {
"directories": [
//変異の対象とするディレクトリ
],
"excludes": [
//変異対象外とするディレクトリ
]
},
変異の種類を絞った
次にやはり発生させる変異の種類を絞りました。以下のように記載することで、変異対象を間引いていくことが可能です。
間引く対象は以下のドキュメントの見出しの項目をキャメルケースで設定ファイル(infection.json5
)に記載します。
"mutators": {
"@default": true,
"@cast": false,
"@function_signature": false,
}
CI時はユニットテストに続けてMutation Testを実行する
InfectionはMutation Testの前にユニットテストを実行します。
ですがCI時においては事前にユニットテストが実行されているため、Mutation Testの実行前にもわざわざ実行する必要がありません。
そのため、--skip-initial-tests
を使ってInfection実行前のユニットテストの実行を省略します。
ただし、テストをスキップするためにはxml
とjunit
の2種類のカバレッジレポートが提供されている必要があるため、以下のようなオプションをつけてPHPUnitを実行する必要があります。
XDEBUG_MODE=coverage php artisan test --coverage-xml=report/coverage/coverage-xml --log-junit=report/coverage/junit.xml
MSIのスコアが一定以下の場合、CIをエラーとする問題
--min-msi
オプションを付けることで、解決できます。
閾値を下回った場合、以下のようなエラーとともに終了します。
CI時に毎回動くため、全てのファイルではなくGit上差分があるファイルだけを対象としたい問題
全ファイルを対象にInfectionを実行した場合、導入環境だと40分弱かかっています。
CIの度に42分も待つのは現実的ではないため、やはりStrykerと同じく差分に対してのみ実行したいです。
そこで--git-diff-filter
と--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.push
とon.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カンファレンスでお話ししたいなぁ・・・
Discussion