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 coverage100%
Branch coverage100%
Function coverage100%
Mutants killed3 / 6
Mutants survived3 / 6
Mutation score50%

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 loop711.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 が自動でテストを増やしていくほど、この確認はむしろ重要になると思います。