カバレッジが測れるのは「書かれたコード」だけ
結論から言うと、カバレッジという指標は「すでに書かれたコードが実行されたかどうか」しか計測できません。
バグの大部分は「書き間違えたコード」から生まれるのではなく、「本来書かれるべきだったのに、書かれなかったコード」に潜んでいます。しかし、カバレッジツールは「ここに本来あるべき分岐が存在しませんよ」とは教えてくれないのです。
具体例:割引率計算のバグ
私が経験した、シンプルな金額計算の関数を例にします。
/**
* 割引適用後の価格を計算する
* @param price 元値
* @param discountRate 割引率
*/
function finalPrice(price: number, discountRate: number): number {
return Math.round(price * (1 - discountRate));
}
ここで、GitHub CopilotなどのAIにテストを書かせてみます。AIは実装を読み取り、数秒で以下のようなテストケースを生成します。
describe('finalPrice', () => {
it('割引率20%の場合、800円になること', () => {
expect(finalPrice(1000, 0.2)).toBe(800); // ✓
});
it('割引率0%の場合、元の価格のままになること', () => {
expect(finalPrice(1000, 0)).toBe(1000); // ✓
});
it('割引率50%の場合、半額になること', () => {
expect(finalPrice(500, 0.5)).toBe(250); // ✓
});
});
この関数には if 文などの分岐が一切ないため、これらのテストを実行した瞬間、C0(行網羅率)もC1(分岐網羅率)もあっさり100%に達します。
しかし、もし finalPrice(1000, 1.5) という入力が来たらどうなるでしょうか?
結果は -500。割引率が100%(1.0)を超えると、価格がマイナスになってしまいます。
恐ろしいのは、このエッジケースを誰もテストしていないのに、カバレッジは100%を維持しているという事実です。
「割引率は0〜1の間であるべき」というドメイン知識(意図)はコードのどこにも書かれておらず、分岐が存在しないため、カバレッジツールも警告の出しようがないのです。
AIがテストを書くと「実装の鏡」になる
ここに、AIを使ってテストを生成する際の特有の罠があります。
AIに「実装を読んでテストを書いて」と指示することは、本質的に「現在のコードの振る舞いをそのまま書き写せ」と命じているのと同じです。結果として、生成されたテストは「実装の完璧な鏡(ミラー)」になります。
鏡は、そこに映った映像が正しいかどうかを判断しません。
もし finalPrice に「1のズレ(Off-by-one)」などのロジックの欠陥が元から潜んでいた場合、AIはそのバグを含んだ出力をそのまま「期待値」としてテストに固定化してしまいます。
- AIがバグの挙動を正としてテストを生成する
- テストがパスし、カバレッジが100%になる
- バグが「仕様」としてシステムに定着する
後から誰かが正しいロジックに修正しようとすると、今度はテストが落ちて「リグレッション(退行)を埋め込んだ!」と怒られる羽目になります。鏡が現実を上書きしてしまうのです。
「実装」ではなく「意図」をテストする
テストの目的は、「コードが現在どう動くか」を記録することではなく、「コードがどう動くべきか」を守ることです。
単に実装をミラーリングするだけのテスト(前者)をいくら量産してカバレッジを100%にしても、本来あるべき仕様(後者)の欠落は1ミリも埋まりません。
テストの照準は「実装」から「意図」へと切り替える必要があります。
とはいえ、日々の開発の中で「仕様書に書かれていない暗黙の前提」をゼロから思い出すのは困難です。そこで必要なのが、「意図をロードする仕組み」です。
実践:バグは「個別の事象」ではなく「カテゴリー」で記録する
本番環境で finalPrice(1000, 1.5) のバグが発覚した時、「割引率1.5のテストケース」を1つ追加してチケットをクローズしてはいけません。
それをすると、来月別の関数で「数量にマイナスが入った」「日数がゼロになった」と、同じ穴に落ちることになります。
これらはすべて、「定義域の境界バリデーション漏れ」という同一カテゴリーのバグです。
私はチームで、過去の障害を以下のような「プロパティ(性質)リスト」としてリポジトリ内に運用し始めました。
# 過去に踏んだ入力エッジケースのカテゴリー
- **定義域の境界**
- 金額、レート、数量は必ず「マイナス値 / 上限超え / ゼロ」をバリデーションすること
- *事例:2026年6月 finalPriceにて割引率1.5でマイナス価格が発生*
- **時間の相対性**
- 「今日」を基準とする分岐ロジックは、必ず日付の境界とタイムゾーンを検証すること
- *事例:2026年5月 月末23時の決済ロジックが前月として計上された*
AIのプロンプトに「過去の記憶」を注入する
このリストを作っただけでは意味がありません。テストを生成する(または書く)瞬間に、このコンテキストが読み込まれる必要があります。
最近ではAIエージェントにテストを書かせることが増えましたが、その際、プロンプトに単に実装コードを渡すのではなく、上記の「プロパティリスト」も一緒にコンテキストとして渡します。
(私はこれを「Memorylake(記憶層)」的なアプローチと呼んでいます)
AIに「実装を読む前に、このプロダクトが過去にどこで火傷をしたのか」をインプットすることで、生成されるテストは単なる「実装の鏡」から脱却し、「境界値に対するバリデーションが抜けていませんか?」と実装を疑う力を持つようになります。
まとめ:カバレッジ100%を見たら疑うべき4つのこと
もしあなたのプロジェクトでカバレッジ100%を達成したら、喜ぶ前に以下のチェックリストを振り返ってみてください。
- この100%は、「コードが正しいこと」の証明か? それとも単に「コードが実行されたこと」の証明か?
- そのテストは、「実装」を読んで書かれたものか? それとも「意図」を読んで書かれたものか?
- 直近のバグは、個別対応でクローズされたか? それとも「カテゴリー」として抽象化して記録されたか?
- そのカテゴリーの記録は、次に人間やAIがテストを書く瞬間、コンテキストとして自動的に提示される仕組みになっているか?
カバレッジ100%は決して旅の終着点ではありません。それは「今書かれているコードは、とりあえず一度全部なぞってみました」という、スタートラインの証明に過ぎないのです。
AIによるコーディング支援が当たり前になった今だからこそ、「意図をどう定義し、どうテストに落とし込むか」というエンジニアリングの基本が、より一層問われていると感じています。
