AI生成テストは信頼できる?Mutation TestingでCoding Agentのテスト品質を確かめる
Claude Code や Codex のような Coding Agent に unit test を書かせること自体は、もうそれほど難しくありません。私が気になるのはその先です。テストは全部 green、Coverage もきれいに上がった。それでも「このテストは本当に間違った実装を捕まえられるのか」は別の話です。
この差は CI が通ると見えにくくなります。Agent が case を追加し、line coverage と branch coverage が上がると、作業は終わったように見えます。ただ、これらの指標が主に示しているのは「コードが実行された」ということです。実装を少しだけ間違った形に変えたとき、テストがちゃんと反対するかまでは直接答えてくれません。
最近公開された mutagate は、その問題に mutation testing を Coding Agent の完了フローへ組み込む形で向き合っています。作者の Reddit 投稿 にある考え方はかなり分かりやすいです。Agent がテストを書いたあと mutation testing を実行し、基準を下回ったらすぐ終了させず、surviving mutants をフィードバックとして返してテストを補強させます。
この発想で特に面白いのは、Agent に返す情報の粒度です。「Coverage 82%」だけでは次に何を直すべきか曖昧です。一方で「age >= 18 を age > 18 に変えてもテストが green のまま」と言われれば、18 という境界を見落としていることがすぐ分かります。
そこで今回は mutagate 自体をインストールせず、自分の Content Lab に小さな Node fixture を書き、仕組みだけを切り出して試しました。結果は思ったよりはっきりしていました。baseline は全テスト成功、line / branch / function coverage はすべて 100% なのに、6 個の controlled mutant のうち kill できたのは 3 個だけでした。
green と 100% Coverage のあとに残る「間違いを拒否できるか」
まず、とても普通の関数を考えます。
export function isAdult(age) {
return age >= 18;
}
テストが次の 2 件だけだとします。
isAdult(20) === true
isAdult(10) === false
true / false の両方を通るので、branch coverage は 100% にできます。
ところが実装を次のように変えても、
return age > 18;
この 2 件のテストはまだ通ります。
Coverage が悪いという話ではありません。Coverage が答えるのは「どの path が実行されたか」です。境界の意味まで assertion が固定できているかは、別の問いです。
Mutation testing はそこを逆側から確認します。たとえば次のような、小さくて意味の分かる間違いを意図的に作ります。
>= -> >
> -> >=
条件を反転
branch の結果を入れ替える
その状態で既存テストをもう一度実行します。
テストが失敗すれば mutant は killed で、その変更をテストが区別できたことになります。テストが green のままなら survived です。Coding Agent にとって survivor が有用なのは、「テストをもっと良くして」という曖昧な指示ではなく、今の suite が区別できなかった具体的な挙動だからです。
この考え方自体は mutagate が最初ではありません。以前の MuTAP の研究 でも、surviving mutants を LLM ベースの test generation に戻してテストを強化しています。今はその feedback loop を Coding Agent の lifecycle に自然に接続できるようになってきました。
Claude Code の現在の Hooks reference には PostToolUse、SubagentStop、TaskCompleted、Stop などの lifecycle event があり、async hook も使えます。つまり「Agent がテストを書く -> deterministic check を走らせる -> 完了してよいか判断する」という流れは、prompt で毎回「自己チェックして」と頼むよりも実装側へ寄せられます。
これは以前書いた CLAUDE.md が大きくなりすぎたときの AGENTS.md、Skills、CI の境界 と同じ考えです。毎回必ず守らせたいものは、少しずつ「モデルが覚えていること」から executable check へ移した方が安定します。
A/B を回すと、Coverage は100%のまま Mutation Scoreだけが50%から100%になった
実験を単純にするため、第三者 repository のコードは実行せず、mutation framework もインストールしていません。自分で書いた Node fixture に、境界を持つ 3 つの小さな関数だけを置きました。
isAdult(age) // age >= 18
discount(total) // total > 100
shipping(total) // total >= 50 ? 0 : 5
そこに 6 個の controlled mutant を手で定義しました。明らかな条件反転と、Coverage だけでは見落としやすい境界変更を混ぜています。
Baseline のテストは 6 件です。各 branch の両側は通しますが、ちょうど境界になる値は意図的にテストしません。
GitHub Actions の結果はこうなりました。
| 指標 | Baseline |
|---|---|
| 通常テスト | PASS |
| Line coverage | 100% |
| Branch coverage | 100% |
| Function coverage | 100% |
| Mutants killed | 3 / 6 |
| Mutants survived | 3 / 6 |
| Mutation score | 50% |
survive した 3 個は、すべて境界の変更でした。
age >= 18 -> age > 18
total > 100 -> total >= 100
total >= 50 -> total > 50
私にとって価値があったのは「50%」という数字より、この 3 行です。次に何を追加すればいいかがすぐ分かるからです。
追加した assertion は 3 件だけでした。
isAdult(18) === true
discount(100) === false
shipping(50) === 0
production code は変えていません。mutation matrix も同じです。再実行すると 6/6 が killed になり、score は 50% から 100% になりました。
ここで「100%」自体を目標にすると、また別の数字遊びになります。役に立ったのは次の loop です。
Agent がテストを書く
-> 通常テスト PASS
-> controlled fault を注入
-> survivors を集める
-> survivors を具体的な feedback にする
-> Agent が boundary / negative assertion を追加
-> 再検証
Coverage は 2 回とも 100% のままで、新しい情報をくれませんでした。survivor list は不足している 3 つの境界をそのまま示しました。
この結果を見て、私は mutation testing を「もう一つの品質スコア」として使うより、Coding Agent に対するfault feedback generatorとして使う方がしっくり来るようになりました。
80%という数字より、survivorをAgentへ返す方が大事
mutagate の現在の default は分かりやすい例です。私が確認した pinned version では mode は gate、mutation threshold は 0.8、maxRounds は 2 です。基準未満なら完了を block して Agent にテストを直させ、mutation infrastructure 自体が壊れた場合は fail-open 側に倒して無限に閉じ込めない設計です。
この方向は理解できますが、私は「80%」をそのまますべての repository の基準にはしません。
mutation score も proxy だからです。
2026 年の LLM 生成テストに関する replication study はかなり慎重な結論を出しています。Coverage や mutation score が real-bug detection をどれだけ表すかは、評価条件に依存します。正しいと仮定したコードに対して複数の test generation 手法を比較する用途では意味のある signal になりますが、code under test 自体に対象 bug が含まれている状況では解釈が難しくなります。
同じく 2026 年の Python class-level test generation の研究 も、structural coverage は天井に張り付きやすく、差が見えにくいと指摘しています。実務的には executability を gate にし、その上で Coverage、mutation testing、structural quality を組み合わせて見る方が自然です。
Coding Agent に mutation testing を入れるなら、まず通常テストに基本的な実行確認と regression を任せます。その次に mutation で「今の assertion では区別できない誤った挙動」を探します。integration、E2E、property、security、domain test は引き続き別の層を受け持ちます。
threshold も tool の default をコピーするより、その repository の baseline から決めたいです。legacy code が多ければ equivalent mutant や unreachable code が増えますし、suite 自体が重い場合もあります。小さな新規 library なら 90% を超えることも難しくないかもしれません。見るべきなのは、重要な path で survivor が繰り返し出ているか、それを意味のある assertion に変えられるかです。
その意味で、Agent に渡すなら score より survivor の方が実用的です。数字は「基準未満」としか言いません。具体的な mutant は「この挙動を今のテストは縛れていない」と教えてくれます。
gateには実行コストがある
Mutation testing はコードを変えてテストを何度も走らせるので、無料ではありません。
今回 GitHub Actions 上で通常テストと mutation loop をそれぞれ 5 回測りました。かなり小さな fixture ですが、中間値は次の通りです。
| 実行 | 中間値 |
|---|---|
| 通常テスト | 98.331 ms |
| Mutation loop | 711.798 ms |
| 追加時間 | +613.467 ms |
| 比率 | 7.24× |
この 7.24× を「mutation testing は通常 7 倍遅い」と一般化してはいけません。あくまでこの小さな synthetic Node fixture と GitHub Actions runner での測定です。ただし、複数の mutant ごとにテストを再実行する以上、追加コストがはっきり見えることは確認できます。
実際の repository なら、最初から every Agent stop で全体 mutation を回すことはしません。Agent が触った module や test に target を絞って fast gate を置き、full mutation suite は CI、夜間実行、または高リスク変更だけに回す方が現実的です。
equivalent mutants と unreachable mutants も厄介です。前者はコードが変わっても意味が変わらず、後者はそもそも実行できません。これらが survived しても、テストが弱いとは限りません。tool がうまく扱えないと Agent は実質的に kill 不可能な数字を追うことになります。
infrastructure error もテスト品質とは分けるべきです。mutation runner の crash、dependency 不足、timeout は「テストが悪い」という証拠ではありません。mutagate が fail-open を選ぶ理由もここにあります。信頼性を上げるための gate が、日常の開発を頻繁に止める gate になってしまうのは避けたいです。
security boundary もあります。mutagate の security documentation は、hook が起動する build / test が user permission で実行され、Agent sandbox 内に自動的に閉じ込められるわけではないことを明記しています。新しい third-party hook を長期開発環境へ入れるなら、アイデアだけを見て導入せず、コードを読み、version を固定し、何を実行するのか把握してから判断したいです。
私ならMutation Testingを「完了の証拠」に置く
今は Coding Agent の test work を 2 つに分けて考えています。
1 つ目はテストを生成することです。現在のモデルは happy path、mock、よくある boundary case をかなり速く書けます。
2 つ目は、その suite を信頼してよいか証明することです。ここまでをモデルの自己評価だけに任せる必要はありません。
green test は最初の gate になります。Coverage は構造上の blind spot を見つけるのに使えます。mutation survivors は assertion が「あり得る間違い」を本当に区別しているか確認します。業務上重要な path では integration、E2E、property、domain test も残ります。
そう考えると Mutation Testing の位置はかなり明確です。新しい真理スコアではなく、Coverage の代替でもありません。「テストを書き終えました」という Agent の完了宣言に、少しごまかしにくい evidence を追加する層です。
mutagate はまだ新しい project なので、この小さな実験だけで production standard とは言えません。ただ、workflow の考え方には納得しています。Agent が終わる前に、テストが通るかだけを見るのではなく、実装を意図的に少し壊して、それでもテストが黙ったままか確認する。
人が書いたテストでも価値のある問いです。Coding Agent が自動でテストを増やしていくほど、この確認はむしろ重要になると思います。