PRの変更差分に対してミューテーションテストをするPythonプロジェクト用のGitHub Actionsを作った話
ミューテーションテスト(Mutation test)とは
ソースコードの一部を故意に変更して、バグを混入させた状態で単体テストを動かし、テストが失敗するかどうかを確認するテスト手法のことです。
- テストが失敗した場合
- バグを検知できているので、該当箇所は
テストで担保されていると言えます。
- バグを検知できているので、該当箇所は
- テストが成功した場合
- バグを検知できていないので、該当箇所は
テストで担保されていないと言えます。
- バグを検知できていないので、該当箇所は
ミューテーションテストにおいて、検知できたバグのことをKilled、検知できなかったバグのことをSurvivedと読んでおり、Survivedの数が多いほど、テストで担保できていない箇所が多く、品質に問題がある可能性があります。
ミューテーションテストのメリット
見かけ上のコードカバレッジが高く、テストの網羅率が高かったとしても、本当にテストが効果的に機能しているか(バグを検知できているか)は分からないので、ミューテーションテストによって、検知できないバグがどれくらいあるのか、担保すべき箇所を正しく担保できているかを確認することができます。
作ったアクションの概要
PRを出した際、ソースコードで変更した行に対してミューテーションテストを実行し、KilledとSurvivedの数や、ファイル内のSurvivedの該当箇所をPRにコメントする、以下のアクションを作りました。
↓以下のようにコメントします。

↓開くとKilledとSurvivedの数や、ファイル内のSurvivedの該当箇所を見れます。

使い方
以下のように、リポジトリをチェックアウトしてから、プロジェクトのlockファイルのパスや、ソース/テスト/テスト実行場所の各ディレクトリ、Pythonのバージョンを指定して、アクションを実行するだけです。
on:
pull_request:
paths:
- "python-project/src/**"
- "python-project/tests/**"
- "python-project/Pipfile.lock"
jobs:
mutation-testing-report:
runs-on: ubuntu-latest
name: Mutation testing report
timeout-minutes: 15
permissions:
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: Century-ss/python-mutation-report@v2
with:
lock-file-path: "python-project/Pipfile.lock"
src-directory: "python-project/src"
test-directory: "python-project/tests"
where-to-run-test: "python-project" # ルートディレクトリでテストを動かす場合は指定不要
python-version: "3.11" # デフォルトは3.11
lockファイルは、Pip・Pipenv・Ryeの各プロジェクトに対応しています。
テストランナーはPytestのみに対応しています。
まとめ
PRの変更差分に対してどこがテストで担保できていないかをすぐに把握できるのは、テスト品質を担保するのに役立つと思います。
「コードカバレッジは高いんだけど、最近バグが多いんだよな🤔」という人は、ぜひ使ってみてください。
Discussion