0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

タイトル:AIが生成するテストは「実装の鏡」になる——カバレッジ100%でもバグが消えない理由

0
Posted at

20260721175253.jpg

カバレッジが測れるのは「書かれたコード」だけ

結論から言うと、カバレッジという指標は「すでに書かれたコードが実行されたかどうか」しか計測できません。

バグの大部分は「書き間違えたコード」から生まれるのではなく、「本来書かれるべきだったのに、書かれなかったコード」に潜んでいます。しかし、カバレッジツールは「ここに本来あるべき分岐が存在しませんよ」とは教えてくれないのです。

具体例:割引率計算のバグ

私が経験した、シンプルな金額計算の関数を例にします。

/**
 * 割引適用後の価格を計算する
 * @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はそのバグを含んだ出力をそのまま「期待値」としてテストに固定化してしまいます。

  1. AIがバグの挙動を正としてテストを生成する
  2. テストがパスし、カバレッジが100%になる
  3. バグが「仕様」としてシステムに定着する

後から誰かが正しいロジックに修正しようとすると、今度はテストが落ちて「リグレッション(退行)を埋め込んだ!」と怒られる羽目になります。鏡が現実を上書きしてしまうのです。

「実装」ではなく「意図」をテストする

テストの目的は、「コードが現在どう動くか」を記録することではなく、「コードがどう動くべきか」を守ることです。

単に実装をミラーリングするだけのテスト(前者)をいくら量産してカバレッジを100%にしても、本来あるべき仕様(後者)の欠落は1ミリも埋まりません。
テストの照準は「実装」から「意図」へと切り替える必要があります。

とはいえ、日々の開発の中で「仕様書に書かれていない暗黙の前提」をゼロから思い出すのは困難です。そこで必要なのが、「意図をロードする仕組み」です。

実践:バグは「個別の事象」ではなく「カテゴリー」で記録する

本番環境で finalPrice(1000, 1.5) のバグが発覚した時、「割引率1.5のテストケース」を1つ追加してチケットをクローズしてはいけません。
それをすると、来月別の関数で「数量にマイナスが入った」「日数がゼロになった」と、同じ穴に落ちることになります。

これらはすべて、「定義域の境界バリデーション漏れ」という同一カテゴリーのバグです。

私はチームで、過去の障害を以下のような「プロパティ(性質)リスト」としてリポジトリ内に運用し始めました。

# 過去に踏んだ入力エッジケースのカテゴリー

- **定義域の境界**
  - 金額、レート、数量は必ず「マイナス値 / 上限超え / ゼロ」をバリデーションすること
  - *事例:2026年6月 finalPriceにて割引率1.5でマイナス価格が発生*
- **時間の相対性**
  - 「今日」を基準とする分岐ロジックは、必ず日付の境界とタイムゾーンを検証すること
  - *事例:2026年5月 月末23時の決済ロジックが前月として計上された*

AIのプロンプトに「過去の記憶」を注入する

このリストを作っただけでは意味がありません。テストを生成する(または書く)瞬間に、このコンテキストが読み込まれる必要があります。

最近ではAIエージェントにテストを書かせることが増えましたが、その際、プロンプトに単に実装コードを渡すのではなく、上記の「プロパティリスト」も一緒にコンテキストとして渡します。
(私はこれを「Memorylake(記憶層)」的なアプローチと呼んでいます)

AIに「実装を読む前に、このプロダクトが過去にどこで火傷をしたのか」をインプットすることで、生成されるテストは単なる「実装の鏡」から脱却し、「境界値に対するバリデーションが抜けていませんか?」と実装を疑う力を持つようになります。

まとめ:カバレッジ100%を見たら疑うべき4つのこと

もしあなたのプロジェクトでカバレッジ100%を達成したら、喜ぶ前に以下のチェックリストを振り返ってみてください。

  1. この100%は、「コードが正しいこと」の証明か? それとも単に「コードが実行されたこと」の証明か?
  2. そのテストは、「実装」を読んで書かれたものか? それとも「意図」を読んで書かれたものか?
  3. 直近のバグは、個別対応でクローズされたか? それとも「カテゴリー」として抽象化して記録されたか?
  4. そのカテゴリーの記録は、次に人間やAIがテストを書く瞬間、コンテキストとして自動的に提示される仕組みになっているか?

カバレッジ100%は決して旅の終着点ではありません。それは「今書かれているコードは、とりあえず一度全部なぞってみました」という、スタートラインの証明に過ぎないのです。

AIによるコーディング支援が当たり前になった今だからこそ、「意図をどう定義し、どうテストに落とし込むか」というエンジニアリングの基本が、より一層問われていると感じています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?