退屈なことはAIにやらせよう(ミューテーションテスト編)
はじめに
株式会社COMPASSでシステムエンジニアをやっている笹島です。
進化し続けるAIエージェントを用いて日々の開発は楽に、その分プロダクトの競争も激しくなっていきます。
そんな中自信を持ってプロダクトを世に送り届けていくには、開発者側も自信を持ってプロダクトに品質を作り込んでいきたいものです。
ということで今回はAIエージェントを用いた品質担保の施策として、ミューテーションテストとAIエージェントを組み合わせた「テストパターン自動追加」の取り組みを共有します。
ミューテーションテストについて
ミューテーションテストとは、テストコードの網羅性を評価するための手法の一種です。
具体的には、プロダクトコードに意図的なバグ(ミュータント)を加え、それに対してすでに実装されているテストコードが失敗するかどうかを確認します。もしテストコードがそのミュータントを検出できなければ、「今後コードにバグを作り込んだ時に自動テストで検知できないのでは?」という疑いが生まれます。
つまり、テストのテストを行うための便利かつ確立された手法です。
しかし実際に導入してみると、ミューテーションテストにはいくつか大きなハードルが存在します。
ミューテーションテストの辛いところ
PRベースのCIには向いていない
まず第一に重いです。
ミューテーションテストは、プロダクトコードの制御式などを少しずつ変異させて繰り返しテストコードを実行するため、テストの総回数が多く通常のユニットテストと比較して実行時間が圧倒的に長くなります。
また、冪等性にも問題があります。
テストコードのように決められたパターンを実行するのではなく、基本的にはプロダクトコードに対して変異可能な部分をランダムに変更させてテストを実施します。
昨日ミュータントを全て駆逐したとしても、明日のミューテーションテストではミュータントが生き残っている可能性が高いです。(そもそもミュータントを全て駆逐するべきかはさておき)
これらの理由から、CIパイプラインで毎回実行するのはあまり現実的ではありません。
差分のみミューテーションテストを実施するなども考えられますが、パイプラインの複雑さを考えるとこちらもあまりおすすめできません。
専用の確認フローが必要になる
PRベースのCIに向いていないとなると、たとえば「週に一度ミューテーションテストを回し、レポートを確認する」といった運用が考えられます。しかしこのアプローチでもスムーズにミューテーションテストを組織に組み込むことは難しいと思っています。
この場合、当番制などの運用を強いなければ「できるメンバーがいつもミューテーションテストの対応を行っている」という状況にもなりがちです。
また、ミューテーションテストそのものの運用負荷として、レポートの読み解きが難しいです。
どのテストパターンが足りないのか、何が検出されなかったのかを見極めるには、ある程度の経験や時間が必要になります。
そのため、せっかく週次で回しても対応の優先順位がつかないまま放置されたり、特定のメンバーに負荷がかかってしまうということが想像できます。
そもそもうまく運用できるビジョンが見えないために、ミューテーションテストしてみたことはあるけどフローとしては組み込むのを諦めていた、ということも割とありそうだと思っています(というか私はこれでした。)
退屈なことはAIにやらせよう(本題)
「レポートの読み解きが難しい」と言いましたが、レポートの結果自体は構造化されたデータです。
しかも「ミューテーションテストの結果からテストパターンを追加する」という作業自体は、原因と対応方法が明確であり、クネビンフレームワークでいうところの「単純系」の作業です。
「ということはこの作業はとてつもなくAIエージェント向きなのではないか」と思いまして、実際にAIエージェントに対応させる取り組みを行いました。
実際にやってみた
取り組んだのはgo言語のプロダクトであり、go-mutestingを利用しています。
以下のようなGithubActionsを構築しました。
name: "Mutation Testing Workflow"
on:
issues:
types: [opened, assigned]
jobs:
mutation-testing:
if: |
(github.event_name == 'issues' && (contains(github.event.issue.body, 'go-mutesting') || contains(github.event.issue.title, 'go-mutesting')))
permissions:
contents: read
pull-requests: write
id-token: write
issues: write
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Set up Go
id: setup-go
uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
cache-dependency-path: "go.sum"
- name: Get Go dependencies
if: ${{ steps.setup-go.outputs.cache-hit != 'true' }}
shell: bash
run: |
go mod tidy
- name: Cache go-mutesting binary
id: cache-mutesting
uses: actions/cache@v4
with:
path: ~/go/bin/go-mutesting
key: mutesting-${{ runner.os }}-${{ hashFiles('**/go.sum') }}
- name: Install go-mutesting
if: steps.cache-mutesting.outputs.cache-hit != 'true'
run: |
go install github.com/avito-tech/go-mutesting/cmd/go-mutesting@latest
- name: Run mutation testing on domain entities
continue-on-error: true
run: |
echo "Running mutation testing on domains..."
# ここはミューテーションテストの対象としたいパッケージを選択してください
go-mutesting --disable "statement/remove" ${TARGET_PACKAGE}
- name: mutation testing results with Claude Code
uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
allowed_tools: "WebFetch,WebSearch,Bash(chmod:*),Bash(git:*),Bash(npm:*),Bash(bun:*),Bash(node:*),Bash(ls:*),Bash(grep:*),Bash(cd:*),Bash(pwd:*),Bash(tail:*),Bash(mkdir:*),Bash(touch:*),Bash(rm:*),Bash(make:*),Bash(go:*)"
direct_prompt: |
## 作業の概要
go-mutestingの結果を元に、不足しているテストパターンの追加を行う
## 具体的な要件
### ミューテーションテストの結果
./report.json
### 対応内容
- ミューテーションテストの結果を解析し、不足しているテストパターンを特定する
- 不足しているパターンが存在しない場合は処理を終了する
- 不足しているパターンが存在する場合はテストパターンを追加する
- 前提として、ミューテーションテストの対象となっているメソッドはカバレッジが100%であることがCIで担保されている
- そのため新たなテストメソッドを実装するのではなく、既存のテストメソッドに対してパターンの追加を行う
- 追加したテストパターンと生き残ったミュータントとの関連性をissueのレスポンスとして出力すること
- プロダクトコードの実装に問題がある場合は、issueのコメントにて指摘する
### 禁止事項
- プロダクトコードの修正
- テストコード自体のリファクタリング
- テストメソッドを追加する
## 参考にすべきドキュメント等
- ./docs/ard/
- https://github.com/avito-tech/go-mutesting
## その他
レスポンスは日本語でお願いします。
プロンプトは現在調整中です。
また、claude-code-actionは現在 schedule
から直接実行できないため、GithubActionsで週次で上述のWorkflowがキックされるissueを作成することで事実上 schedule
から実行するというプロキシ方式をとっています。
DevinであればAPIを実行するだけで同様の仕組みも実現できるのですが、現状PRの質がClaudeCodeの方が良いと感じているため、claude-code-actionを利用している状態です。
この辺りは対象とするリポジトリによって調整が必要な部分だと思いますので、皆さまが導入される際には自身のプロダクトにあったプロンプトを探求してください。
上がってくるコメント・PRの雰囲気
まずissueのコメントとして、以下のような統計情報を残してくれます。
統計情報
総変異数: 283個
殺された変異数: 259個 (91.5%)
逃げた変異数: 24個 (8.5%)
MSI (変異スコア): 0.9152
これだけでも人力でレポート確認するよりありがたい。
でもそれだけではなく、さらに以下の詳細をコメントした上でPRを作成してくれます。
- 追加したテストパターン
- 生き残ったミュータントとの関連性
先述のプロンプトにも記載しておりましたが、現在このプロダクトではテーブル駆動のユニットテストをメインに記述しており、ミューテーションテストの対象とするDomain層のUTはCIでカバレッジ100%を担保しております。(カバレッジ100%というとマサカリが飛んできそうで怖いけど)
そのため、生き残ったミュータントに対するテストパターンを追加する場合も、テーブルに要素を追加してくるだけなのでPRも読みやすく不足していたパターンもわかりやすいため、現段階ではこのプロダクトとは非常に高い親和性を感じております。
今後増え続けるテストコードに対しても活躍し続けてくれることに期待。
全くの余談ですが、ミュータントは殺す(kill)ことを前提としているため、AIは容赦無く「生き残ったミュータントは〇〇です。今回の対応でミュータントを殺すことができます」とかコメントしてきます。この業界あるあるですが飛び交う言葉が物騒で怖い。
まとめ
ミューテーションテストは、理論的には非常に強力な手法です。しかし私はミューテーションテストを効果的に運用できたことがありませんでした。
今回ご紹介した取り組みは、その課題に対してAIエージェントの活用という現実的な解決策を模索したものです。
- 定期的にミューテーションテストを実行
- レポートを自動解析してAIにテストコードを提案させる
- 自動でPRを作成して、開発者はレビューに専念
という流れが整うことで、「せっかくミューテーションテストを回しても対応できない」という状況に陥ることなく運用していけると考えています。
AIエージェントはコーディング力に注目されておりますが、このようなプロダクトの継続的な評価と改善にも非常に効果を発揮していってくれます。
今後も、AIを利用した継続的な評価と改善が可能な要素を発見していき、新たな取り組みを進めていきたいと考えています。
終わりに
株式会社COMPASSでは一緒に教育をより良くしていく仲間を募集しています。
少しでも興味を持っていただけた方は、以下よりお気軽にご応募ください。
とりあえず話をきいてみたい!という方はぜひカジュアル面談に来ていただけると幸いです。
Discussion