「カバレッジ 100% です」と書かれたテスト結果を受け取って安心した直後に、本番でバグが出る——受託開発の引き継ぎや検収の現場で、何度も見てきた光景です。カバレッジの数字は嘘をついていません。問題は、その数字が「コードが実行されたこと」しか示しておらず、「テストが正しく中身を検証したこと」までは保証しない点にあります。テストが対象コードを通っていても、肝心の assert(期待値の確認)が弱ければ、バグはすり抜けます。
この「カバレッジの罠」を可視化する手法として、2026 年に再評価されているのが Mutation Testing(ミューテーションテスト)です。Zenn の カバレッジ100%でもバグは止められない——「テストの量」から「テストの質」への転換 や、Thoughtworks の Technology Radar Vol.34(2026 年 4 月)で Trial 入りしたことが、この潮流を象徴しています。とりわけ大きいのは、LLM がテストを大量生成するようになり、カバレッジが品質指標として一段と当てにならなくなったことです。AI が書いたテストが「通っているけれど何も検証していない」ケースを、人間が目視で見抜くのは困難です。受託で納品物の品質を保証する立場では、Mutation Testing は「テストのテスト」として、いま最も現実的な答えのひとつだと考えています。
カバレッジが「質」を保証しない理由
カバレッジは、テスト実行中にどの行・分岐が通ったかを数えるだけの指標です。次のようなテストでも、カバレッジは 100% になり得ます。
// 対象: 割引額を計算する関数
function calcDiscount(price: number, rate: number): number {
if (rate > 0.5) return price * 0.5; // 上限50%
return price * rate;
}
// "通るだけ" のテスト(カバレッジ100%だが検証が弱い)
test('calcDiscount runs', () => {
calcDiscount(1000, 0.3); // 呼ぶだけ
calcDiscount(1000, 0.8); // 呼ぶだけ
expect(true).toBe(true); // 何も検証していない
});
このテストは両方の分岐を通るのでカバレッジ 100% ですが、戻り値を一切検証していないため、return price * 0.5 を return price * 0.4 に書き換えてもテストは通ります。つまりバグを 1 つも止められません。カバレッジは「Goodhart の法則」——指標を目標にした瞬間に指標としての意味を失う——の典型例で、目標値にした途端、こうした空のテストで数字だけ満たす圧力が生まれます。
Mutation Testing は何をするのか
Mutation Testing は、対象コードにわざと小さなバグ(ミュータント)を仕込み、既存のテストがそれを検知して失敗するかを確かめます。検知できれば「ミュータントを殺した(killed)」、検知できずテストが通ってしまえば「生き残った(survived)」と判定し、生き残ったミュータント=テストの穴として可視化します。
| 観点 | コードカバレッジ | Mutation Testing |
|---|---|---|
| 測るもの | コードが実行されたか | テストがバグを検知できるか |
| 弱い assert | 検出できない | 生き残りとして検出 |
| 空のテスト | 100% になり得る | スコアが下がり露見 |
| LLM 生成テスト | 質を判別できない | 質を定量化できる |
| 主な用途 | 実行範囲の把握 | 検証力の保証 |
たとえば「> を >= に変える」「return a + b を return a - b に変える」「条件を反転する」といった変異を自動で多数仕込み、テストスイートがどれだけ殺せたかを Mutation Score(殺した割合)として出します。KAKEHASHI の事例 LLMが生成したテストの品質をMutation Testingで検証する のように、AI が書いたテストの実効性を測る用途で導入する組織が増えています。受託で AI を使ってテストを量産するなら、その質を担保する仕組みはセットで必要です。AI 生成物の品質保証という観点は QA自動化の受託導入(GH Media) とも地続きです。
導入の最小例(Stryker)
JavaScript/TypeScript なら Stryker Mutator が定番です。既存の Jest や Vitest のテストにそのまま被せられます。
# 1) 導入
npm i -D @stryker-mutator/core @stryker-mutator/vitest-runner
# 2) 設定(stryker.conf.json の最小例)
# - 重要モジュールだけを対象に、まず小さく始める
# - しきい値で CI を制御(high=合格 / break=失敗)
cat > stryker.conf.json <<'JSON'
{
"testRunner": "vitest",
"mutate": ["src/domain/**/*.ts"],
"thresholds": { "high": 80, "low": 60, "break": 60 }
}
JSON
# 3) 実行 → Mutation Score を出力
npx stryker run
ポイントは、最初から全コードに掛けないことです。Mutation Testing は通常のテストより実行が重いため、まずは金額計算・在庫・権限判定など「間違えると損害が出る中核ロジック」に絞り、CI では break しきい値で品質を機械的に守ります。Vitest 環境での導入は Vitest 4.1 を受託に取り入れる(GH Media) も参考になります。
受託でどう使うか — 「テストがあります」を「バグを止めます」に変える
検収やレビューで「テストを書きました」「カバレッジ 90% です」という報告は、それ自体では品質を保証しません。受託の納品物として一段上の約束をするために、弊社では次の使い方をします。
中核ロジックには Mutation Score の下限を契約・CI の合格条件に組み込みます。「カバレッジ 80% 以上」だけでなく「重要モジュールの Mutation Score 70% 以上」を満たさなければ CI が落ちる、という形です。これにより、「実行されただけのテスト」では合格できない状態を作れます。あるバックエンド開発のクライアントでは、引き継いだコードのカバレッジは高かったものの、料金計算ロジックの Mutation Score が極端に低く、assert が金額を検証していないテストが大量に見つかりました。ここを補強したことで、その後のリリースで料金まわりのバグがほぼ出なくなりました。
LLM でテストを量産する案件では、生成直後に Mutation Testing を一度回し、生き残ったミュータントを潰す工程を標準にします。これが、AI 生成テストを「通っているだけ」で終わらせないための歯止めになります。
ハマりやすい落とし穴
第一に、全コードに一気に掛けて実行時間が爆発すること。重いので対象を中核ロジックに絞り、CI では差分や夜間実行に回します。第二に、Mutation Score 100% を目標にしてしまうこと。等価ミュータント(意味的に同じで殺せない変異)が必ず混じるため、現実的なしきい値(中核 70〜80% など)で運用します。第三に、生き残りの放置。可視化しても潰さなければ意味がないので、生き残りはレビューで「テスト追加」か「等価と判断」かを必ず仕分けます。E2E まで含めた品質確保は Playwright × AI で QA を自動化する受託(GH Media) も併読してください。
まとめ — 数字を「実行率」から「検知力」へ
カバレッジ 100% は「全部テストした」ではなく「全部の行を通った」に過ぎません。本番でバグを止めるのは、コードを通すことではなく、変更を検知できる強いテストです。Mutation Testing はその検知力を定量化し、LLM が量産するテストの質まで見える化します。受託で品質を保証する立場では、中核ロジックに Mutation Score の下限を設け、CI の合格条件に組み込み、生き残ったミュータントを潰す運用が、「テストがあります」を「バグを止めます」に変える現実的な一手です。
「カバレッジは高いのに本番でバグが出る」「AI が書いたテストの質が不安」というご相談は お問い合わせフォーム からお気軽にどうぞ。中核ロジックの Mutation Testing 導入から始められます。