AIにテストを書かせても意味がない?実装をそのまま写すテストを防ぐ方法

AIにテストを書かせても意味がない?実装をそのまま写すテストを防ぐ方法 AI開発

Claude Code、Codex、Gemini CLIなどのAIコーディングツールへ実装を依頼すると、そのままテストコードまで生成できます。

たとえば、

依頼例
この関数を実装して、
Vitestのテストも追加してください。

と依頼すれば、実装とテストを一度に作れるため非常に便利です。

しかし、ここには大きな落とし穴があります。

AIが実装コードを読んで、その処理をそのままテストコードへ書き直しただけなら、

危険なパターン
実装にバグがある
↓
テストにも同じ考え方が入る
↓
テスト成功

という状態になるからです。

テストが10件、100件成功していても、実装の間違いをそのまま「正しい仕様」として固定しているだけなら品質保証にはなりません。

OpenAIのCodex公式Best Practicesでも、コード変更だけで終了せず、必要なテストを作成し、関連チェックを実行し、結果を確認し、最後に変更内容をレビューする流れが推奨されています。ただし、Codexが何を「正しい」と判断するかについては、PromptやAGENTS.mdなどから明確な基準を与える必要があります。

重要なのは、

AIにテストを書かせることではなく、実装とは独立した「正しさの基準」をAIへ渡すことです。

この記事では、AIが実装をそのまま写しただけの無意味なテストを作る原因と、仕様起点、ブラックボックステスト、境界値、Property-Based Testing、Mutation Testingを使って「本当にバグを検出できるテスト」を作る方法を解説します。Claude Code固有のTDD 3エージェント構成(RED/GREEN/REFACTOR)やHooksによるテスト強制の実装はClaude Codeテスト自動化・TDD完全ガイドで解説しているため、この記事ではツールを横断する技法として、仕様ベースの期待値設計・Property-Based Testing・Mutation Testingを中心に掘り下げます。

スポンサーリンク
  1. AIにテストを書かせること自体は意味がある
  2. 最も危険なのは実装と期待値が同じ計算をしているテスト
  3. 期待値は実装ではなく仕様から固定する
  4. AIには実装より先に仕様を渡す
  5. 実装担当とテスト担当を分ける
  6. テスト名を先に作らせる
  7. 境界値を必ずテストさせる
  8. 正常系だけでなく失敗条件を仕様化する
  9. Private Methodを直接テストしすぎない
  10. Mockの呼び出し回数だけをテストしない
  11. UIテストではdata-testidだけに頼らない
  12. E2Eでは内部APIではなくユーザーの結果を見る
  13. テーブル駆動テストで仕様を固定する
  14. Golden Test Caseを人間が用意する
  15. Property-Based Testingで具体例から離れる
  16. Property-Based Testingでも実装の写経は避ける
  17. Code Coverageだけでテスト品質を判断しない
  18. Mutation Testingで「テストが本当に壊れるか」を確認する
  19. StrykerでAI生成テストを検査する
  20. AIにMutation Testingの結果を渡して改善させる
  21. 重要なBugをわざと入れてTestが落ちるか確認する
  22. Snapshot Testだけで安心しない
  23. Testが実装のFunction名に依存しすぎていないか確認する
  24. Testだけを読んで仕様を説明できる状態にする
  25. AIに「何をMockしないか」まで指定する
  26. AIにテストを書かせるプロンプト例
  27. さらに厳しくするならテストケースだけ先に生成する
  28. AIがテストと実装を同時に変更したらDiffを分けて見る
  29. Regression TestではBugを再現してから修正する
  30. Coverage ThresholdとMutation Testingを役割分担する
  31. 全ファイルにMutation Testingする必要はない
  32. AIに大量のテストを作らせるより少数の強いテストを優先する
  33. テストコードにもコードレビューが必要
  34. テスト実行だけでなく実際のアプリでも確認する
  35. AI生成テストに関するよくある質問
  36. まとめ

AIにテストを書かせること自体は意味がある

最初に結論から言えば、AIにテストを書かせること自体が無意味なわけではありません。

テストケースの雛形作成、入力パターンの洗い出し、Mockの作成、既存テスト形式への変換、Regression Testの追加などはAIと相性がよい作業です。

問題になるのは、

問題のある流れ
実装コード
↓
AIが読む
↓
実装と同じロジックで期待値を作る

場合です。

テストコードの目的は実装内容を説明することではありません。

「この入力なら外部からどんな結果が観測されるべきか」を確認するものです。

Testing Libraryでは、テストは実際のユーザーによる利用方法へ近づけるほど信頼性を高められるという原則を掲げ、Component内部の実装詳細へ依存したテストを避けるよう案内しています。

Playwrightも同様に、自動テストではユーザーから見えるBehaviorを確認し、Function名、内部Array、CSS Classなど、ユーザーが通常認識しないImplementation Detailへ依存しないことをBest Practiceとしています。

AIにテストを書かせる場合も同じです。

実装を正解としてテストするのではなく、仕様を正解として実装をテストします。

最も危険なのは実装と期待値が同じ計算をしているテスト

分かりやすい例を見てみます。

ECサイトの送料を計算する関数があるとします。

仕様は「5,000円以上なら送料無料、それ未満なら550円」です。

AIが次のコードを生成しました。

src/shipping.ts
export function calculateShipping(
  total: number,
): number {
  if (total > 5000) {
    return 0;
  }

  return 550;
}

一見すると正しそうですが、仕様は、

仕様
5,000円以上

です。

実装は、

実装
total > 5000

なので、ちょうど5,000円の場合に550円を返すBugがあります。

ところがAIが次のテストを書いたとします。

問題のあるテスト
import {
  expect,
  it,
} from "vitest";

import {
  calculateShipping,
} from "./shipping";

it(
  "送料を計算する",
  () => {
    const total = 6000;

    const expected =
      total > 5000
        ? 0
        : 550;

    expect(
      calculateShipping(total),
    ).toBe(expected);
  },
);

このテストは成功します。

しかし期待値を、

問題の箇所
total > 5000
  ? 0
  : 550

で作っています。

つまり実装と同じロジックをもう一度書いただけです。

元のコードに、

バグの原因
>

というBugがあれば、テストにも同じBugが入り込む可能性があります。

テストファイルが存在していても、Bug検出能力はほとんどありません。

期待値は実装ではなく仕様から固定する

先ほどのテストなら、期待値を計算する必要はありません。

仕様から直接書きます。

shipping.test.ts
import {
  describe,
  expect,
  it,
} from "vitest";

import {
  calculateShipping,
} from "./shipping";

describe(
  "calculateShipping",
  () => {
    it(
      "4,999円なら550円",
      () => {
        expect(
          calculateShipping(
            4999,
          ),
        ).toBe(550);
      },
    );

    it(
      "5,000円なら無料",
      () => {
        expect(
          calculateShipping(
            5000,
          ),
        ).toBe(0);
      },
    );

    it(
      "5,001円なら無料",
      () => {
        expect(
          calculateShipping(
            5001,
          ),
        ).toBe(0);
      },
    );
  },
);

これなら、

実装
total > 5000

という実装では5,000円のテストが失敗します。

テスト側には送料計算ロジックがありません。

あるのは、

仕様から導いた期待値
4999 → 550
5000 → 0
5001 → 0

という仕様から導いた具体的な期待値だけです。

これが実装をそのまま写すテストを防ぐ基本になります。

AIには実装より先に仕様を渡す

AIへ、

曖昧な依頼
このコードのテストを書いて

とだけ依頼すると、AIが利用できる正解情報は実装コードだけになります。

結果として、

ありがちな流れ
コードを読む
↓
コードのBehaviorを推測
↓
そのBehaviorをテストする

という流れになりやすくなります。

これを防ぐには、テスト作成時に仕様を明示します。

たとえば、

仕様を明示する依頼例
calculateShippingの仕様は以下です。

購入金額が5,000円以上なら送料0円です。
5,000円未満なら送料550円です。
負の購入金額は不正入力としてErrorにしてください。

実装コードを正解だと仮定しないでください。
仕様から期待値を作り、
境界値を含むVitestのテストを書いてください。

とします。

AIが参照する「正解」を、

Implementation
Implementation

から、

Specification
Specification

へ移します。

Codex公式Best Practicesでも、AIが正しい結果を判断できるようにするには、GoalだけでなくConstraintsや完了条件をPromptまたはAGENTS.mdへ具体的に与えることが重要だとされています。

実装担当とテスト担当を分ける

さらに独立性を高めたい場合は、実装とテストを同じ依頼で作らせない方法があります。

最初のAIセッションでは実装だけを行います。

次のセッションでは、

依頼例
仕様を基準にテストケースを設計してください。
実装コードの現在の挙動を正しいとは仮定しないでください。

としてテストを作ります。

可能であれば、テスト設計段階では実装の細部より先に仕様書、API Contract、Issue、Acceptance Criteriaを読ませます。

同じAIモデルを使っていても、

実装を作る役割
実装を作る役割

と、

実装を痑う役割
実装を疑う役割

を分離するだけで、少なくとも「自分が生成したコードを自分で正しいと証明する」構造を減らせます。

Codexには変更を実装するAgentとは別に/reviewで専用Reviewerを動かし、Working Treeを変更せず差分を検査する機能があります。

テストでも同様に、実装者と検証者の役割を分ける考え方が有効です。

テスト名を先に作らせる

すぐTest Codeを書かせず、

依頼例
まだコードを書かないでください。
この仕様で必要なテストケース名だけを考えてください。

と依頼する方法もあります。

送料なら、AIにはまず、

テスト観点の例
5,000円未満なら送料が発生する
5,000円ちょうどなら送料無料になる
5,000円を超えると送料無料になる
負の金額は拒否される

という観点を作らせます。

その内容を人間が確認したあとに、

実装依頼
このテストケースをVitestで実装してください。

と依頼します。

いきなりImplementationを渡してTest Codeを生成するより、

段階的な流れ
仕様
↓
Test Case設計
↓
人間確認
↓
Test Code

という段階を挟むことで、Implementation Detailsの写経を減らせます。

境界値を必ずテストさせる

AIが生成するテストでありがちなのが、

不十分なテスト
普通の値では成功する

ことだけを確認するケースです。

先ほどの送料なら、

不十分な例
expect(
  calculateShipping(10000),
).toBe(0);

だけでは、

区別できない例1
total > 5000

と、

区別できない例2
total >= 5000

を区別できません。

重要なのは境界です。

仕様に、

仕様
5,000円以上

とあるなら、

確認すべき値
4999
5000
5001

を確認します。

AIへテストを依頼するときも、

指定例
等価クラスだけでなく、
境界値の直前、境界値そのもの、直後を含めてください。

と明示します。

Mutation Testingでも、>=>へ変更したMutantが生き残る場合、まさにこのような境界条件を検出できるテストが不足していることが分かります。Strykerではコードへ小さな変更を加え、その変更でもTest Suiteが成功した場合、そのMutantはsurvivedとして扱われます。

正常系だけでなく失敗条件を仕様化する

AIへ、

依頼例
ユーザー登録処理のテストを書いて

と依頼すると、

偵った例
正しいメールアドレスで登録できる

だけになることがあります。

しかし実際にBugが入りやすいのは失敗側です。

たとえばメールアドレス登録なら、

バリデーション例
const EmailSchema =
  z.string()
    .email();

というValidationがあるとしても、テストで確認したいのはValidation Libraryを呼んだことではありません。

外部Behaviorを確認します。

user.test.ts
it(
  "不正なメールアドレスを拒否する",
  async () => {
    const response =
      await createUser({
        email:
          "not-an-email",
      });

    expect(
      response.ok,
    ).toBe(false);
  },
);

仕様上拒否すべき値を先に定義しておけば、AIが現在のImplementationに合わせて期待値を変更しにくくなります。

Private Methodを直接テストしすぎない

AIはCode Coverageを増やそうとして、ClassのPrivate Methodや内部Helperを細かくテストしようとすることがあります。

しかし内部構造へ強く結びついたTestは、Behaviorがまったく変わっていなくてもRefactoringだけで壊れます。

Testing LibraryはImplementation Detailへ依存するテストを避ける方針を明確にしています。

PlaywrightのComponent Testingでも、Component内部MethodやInstanceへ直接アクセスするのではなく、ユーザー視点で操作し、その結果をDOMなどの外部Behaviorから確認する方法が推奨されています。

Unit Testでも同じ考え方を利用できます。

たとえば、

src/price-service.ts
class PriceService {
  private calculateTax() {
    // ...
  }

  public getTotal() {
    // ...
  }
}

なら、PrivateなcalculateTax()を直接テストするより、

getTotal()
getTotal()

へ入力を与え、公開された結果が仕様どおりか確認します。

Mockの呼び出し回数だけをテストしない

AI生成テストでは、次のようなTestが大量に作られることがあります。

呼び出し回数のテスト
expect(
  repository.findById,
).toHaveBeenCalledTimes(1);

expect(
  repository.findById,
).toHaveBeenCalledWith(
  "user-1",
);

必要な場合もありますが、これだけでは、

保証されていないこと
ユーザーへ正しい結果が返る

ことを保証していません。

さらに内部実装が、

変更前
findByIdを1回呼ぶ

から、

変更後
Cache確認
↓
必要な場合だけfindById

へ変われば、外部仕様が同じでもTestが失敗します。

Mockの利用自体が悪いわけではありません。

ただしAIには、

指定例
内部関数の呼び出し回数より、
公開APIから観測できる結果を優先して検証してください。

と指示します。

UIテストでPlaywrightがユーザー可視Behaviorを推奨しているのも、同じ考え方です。

UIテストではdata-testidだけに頼らない

React ComponentをAIにテストさせると、

Component
<button
  data-testid="submit-button"
>
  保存
</button>

に対して、

楽ですが弱い
screen.getByTestId(
  "submit-button",
);

を生成する場合があります。

動作はしますが、

離れていること
ユーザーが何を認識して操作しているか

とは離れています。

Testing LibraryはQueryを選ぶ際にも、実際のユーザー操作へ近いものを優先するよう推奨しています。

たとえば、

推奨される方法
const button =
  screen.getByRole(
    "button",
    {
      name: "保存",
    },
  );

として、

クリック
await user.click(
  button,
);

します。

その後、

確認
expect(
  screen.getByText(
    "保存しました",
  ),
).toBeVisible();

と確認します。

これなら内部のCSS ClassやComponent構造が変わっても、ユーザーから見える仕様が同じならTestを維持できます。

E2Eでは内部APIではなくユーザーの結果を見る

Playwrightも、自動テストではユーザーに見えるBehaviorを検証し、内部実装へ依存しないよう推奨しています。

たとえばログイン画面なら、

login.spec.ts
await page
  .getByLabel(
    "メールアドレス",
  )
  .fill(
    "user@example.com",
  );

await page
  .getByLabel(
    "パスワード",
  )
  .fill(
    "password",
  );

await page
  .getByRole(
    "button",
    {
      name:
        "ログイン",
    },
  )
  .click();

await expect(
  page.getByRole(
    "heading",
    {
      name:
        "マイページ",
    },
  ),
).toBeVisible();

と確認します。

AIが実装した、

loginUser()
loginUser()

というFunctionが呼ばれたことをE2E Testで確認する必要はありません。

重要なのは、ユーザーがログイン後の画面へ到達できたかです。

テーブル駆動テストで仕様を固定する

条件分岐が多い場合は、Test CaseをDataとして先に定義するとImplementationを写しにくくなります。

shipping.test.ts
import {
  describe,
  expect,
  it,
} from "vitest";

import {
  calculateShipping,
} from "./shipping";

const cases = [
  {
    total: 0,
    expected: 550,
  },
  {
    total: 4999,
    expected: 550,
  },
  {
    total: 5000,
    expected: 0,
  },
  {
    total: 5001,
    expected: 0,
  },
  {
    total: 10000,
    expected: 0,
  },
];

describe(
  "calculateShipping",
  () => {
    for (
      const testCase
      of cases
    ) {
      it(
        `${testCase.total}円の送料`,
        () => {
          expect(
            calculateShipping(
              testCase.total,
            ),
          ).toBe(
            testCase.expected,
          );
        },
      );
    }
  },
);

expectedはImplementationで計算していません。

仕様書やAcceptance Criteriaから決めた値を固定します。

この形ならAIへ、

指定例
このTest Case表は変更しないでください。
実装が失敗する場合は実装側を直してください。

と指示できます。

Golden Test Caseを人間が用意する

特に重要なロジックでは、AIへ期待値の決定まで任せない方法があります。

たとえば請求額、税金、ポイント、在庫、権限判定などです。

人間が先に、

src/golden-cases.ts
const goldenCases = [
  {
    input: {
      price: 1000,
      quantity: 2,
    },
    expected: 2000,
  },
  {
    input: {
      price: 500,
      quantity: 0,
    },
    expected: 0,
  },
];

を用意します。

AIには、

依頼例
このGolden Caseは仕様として固定されています。
値を変更せず、このCaseを使ってTest Suiteを構築してください。

と依頼します。

実装とテストの両方をAIに作らせる場合でも、Test Oracleだけを人間が固定すれば共倒れを防ぎやすくなります。

Property-Based Testingで具体例から離れる

Example-Based Testだけでは、人間やAIが想像した入力しか検証できません。

そこでProperty-Based Testingを利用する方法があります。

JavaScript/TypeScriptではfast-checkを利用できます。

fast-check公式ドキュメントでは、Example-Based Testが特定の入力と期待値を確認するのに対し、Property-Based Testは入力と出力の間で常に成立すべき関係を定義し、多数の入力を生成して検証するものと説明されています。

たとえば割引率を適用する関数なら、

src/discount.ts
export function applyDiscount(
  price: number,
  rate: number,
): number {
  return price *
    (1 - rate);
}

について、

成立すべき性質
割引率が0〜1なら、
結果は0以上で元価格以下

という性質があります。

discount.property.test.ts
import fc
  from "fast-check";

import {
  expect,
  it,
} from "vitest";

import {
  applyDiscount,
} from "./discount";

it(
  "割引後価格は0以上で元価格以下",
  () => {
    fc.assert(
      fc.property(
        fc.double({
          min: 0,
          max: 1_000_000,
          noNaN: true,
        }),

        fc.double({
          min: 0,
          max: 1,
          noNaN: true,
        }),

        (
          price,
          rate,
        ) => {
          const result =
            applyDiscount(
              price,
              rate,
            );

          expect(
            result,
          ).toBeGreaterThanOrEqual(
            0,
          );

          expect(
            result,
          ).toBeLessThanOrEqual(
            price,
          );
        },
      ),
    );
  },
);

ここではImplementationと同じ計算式をTestへコピーしていません。

仕様上成立すべき性質を確認しています。

fast-checkは多数の入力を生成し、失敗した場合は問題を再現できる小さな入力へShrinkする仕組みも提供しています。

Property-Based Testingでも実装の写経は避ける

Property-Based Testingを使っても、PropertyそのものがImplementationと同じなら意味が薄れます。

悪い例は、

悪い例
fc.property(
  priceArb,
  rateArb,
  (
    price,
    rate,
  ) => {
    expect(
      applyDiscount(
        price,
        rate,
      ),
    ).toBe(
      price *
        (1 - rate),
    );
  },
);

です。

もし、

price × (1 - rate)

というFormula自体が実装と同じ誤解から生まれているなら、同じBugを固定します。

Propertyには、

別の角度
値の範囲

単調性

可逆性

順序

不変条件

など、Implementationとは別の角度から成立する性質を利用します。

fast-checkの公式ドキュメントも、Property-Based Testingの目的は単にRandom Dataを生成することではなく、Behaviorとして成立すべきPropertyを大量のInputで検証してBugを見つけることだと説明しています。

Code Coverageだけでテスト品質を判断しない

AIはCoverageを上げることが得意です。

未実行行を探して、

目的
このif文を通すTest

を追加すればCoverage Percentageを上げられます。

VitestではLine、Function、Branch、Statement CoverageにThresholdを設定でき、たとえばLine Coverage 90%未満なら失敗させる設定が可能です。

しかしCoverageが示すのは、

Coverageが示すこと
そのコードがTest実行中に通ったか

という情報です。

その行の計算結果が正しく検証されたかまでは別問題です。

たとえば、

弱いアサーション
const result =
  calculateShipping(
    5000,
  );

expect(
  result,
).toBeDefined();

でも対象行は実行できます。

しかし、

強いアサーション
expect(
  result,
).toBe(0);

よりBug検出能力は低くなります。

Coverageは「Testされていない場所を探す」指標として使い、Test Qualityそのものの証明にはしないほうがよいでしょう。

Mutation Testingで「テストが本当に壊れるか」を確認する

AI生成テストの品質確認で特に有効なのがMutation Testingです。

Mutation TestingではProduction Codeへ意図的に小さな変更を入れます。

たとえば、

変更前
total >= 5000

を、

変更後
total > 5000

へ変更します。

または、

変更前
return true;

を、

変更後
return false;

へ変更します。

その状態でTest Suiteを実行します。

Strykerでは、このような変更をMutantと呼び、Mutantを入れたことで少なくとも1つのTestが失敗すればkilled、すべて成功したままならsurvivedとして扱います。

つまり、

killedの例
Production Codeを壊した
↓
Testが失敗した

なら、そのTestにはBug検出能力があります。

逆に、

survivedの例
Production Codeを壊した
↓
全Test成功

なら、Coverageが高くてもTest Suiteに弱い部分が残っています。

StrykerでAI生成テストを検査する

JavaScript/TypeScriptならStrykerJSを利用できます。

基本的な考え方は、

Mutation Testing実行
npx stryker run

でMutation Testingを実行し、生き残ったMutantを確認することです。

たとえば次の実装があるとします。

src/is-adult.ts
export function isAdult(
  age: number,
): boolean {
  return age >= 18;
}

AIが、

is-adult.test.ts
it(
  "成人判定",
  () => {
    expect(
      isAdult(20),
    ).toBe(true);
  },
);

だけを書いた場合、

age >= 18
age >= 18

を、

age > 18
age > 18

へ変えても20歳では結果が同じです。

Mutantが生き残る可能性があります。

そこで、

is-adult.test.ts
it(
  "18歳は成人",
  () => {
    expect(
      isAdult(18),
    ).toBe(true);
  },
);

を追加します。

こうすると境界条件を変更したMutantを検出できます。

Mutation Testingは「Testが存在するか」ではなく、「Production Codeが間違ったときにTestが本当に失敗するか」を調べる方法です。StrykerもMutation Testingを「テストをテストする」ための仕組みとして説明しています。

AIにMutation Testingの結果を渡して改善させる

Mutation TestingもAIと組み合わせられます。

たとえばStrykerから、

Survived mutant
age >= 18
↓
age > 18

という結果が出たら、AIへ、

依頼例
このMutantがSurvivedしています。

Production Codeはまだ変更しないでください。

現在の仕様を確認し、
なぜ既存テストでこの変更を検出できないのか説明してください。
その後、このMutantを検出できる最小のテストケースを追加してください。

と依頼します。

ポイントは、

だけにしない
Mutation Scoreを上げて

だけにしないことです。

AIがProduction Code側をMutation Toolに都合よく変更するのではなく、

考えさせること
Test Suite側に何が不足しているか

を考えさせます。

重要なBugをわざと入れてTestが落ちるか確認する

Mutation Testing Toolを導入しなくても、簡単な確認はできます。

たとえば、

元の実装
return total >= 5000
  ? 0
  : 550;

を一時的に、

意図的な変更
return total >= 5000
  ? 550
  : 0;

へ変更します。

その状態で、

テスト実行
npm test

を実行します。

全Testが成功してしまったら、送料計算のTest SuiteはProduction Behaviorを十分に確認できていません。

Testを追加して失敗するようにしたあと、Implementationを元へ戻します。

この発想を自動化したものがMutation Testingです。

Snapshot Testだけで安心しない

AIは複雑なObjectやUIへ対してSnapshot Testを作ることがあります。

Snapshotが意図した仕様を固定できるケースはありますが、

形骨化した運用
大量のSnapshot Diff
↓
AIが全部更新
↓
Test成功

という運用になると意味が薄れます。

リファクタリング後にSnapshotが変わった場合は、

確認すべきこと
なぜ変わったのか

を確認します。

ユーザーが見るべき重要な値については、

明示的なAssertion
expect(
  result.status,
).toBe("active");

expect(
  result.total,
).toBe(5000);

のように明示的なAssertionを併用すると、意図しない変更を見つけやすくなります。

Testが実装のFunction名に依存しすぎていないか確認する

次のようなTestがあるとします。

依存の強い例
expect(
  service
    .calculateInternalPrice,
).toHaveBeenCalled();

リファクタリングで、

変更前の名前
calculateInternalPrice

を、

移動先
PricingService

へ移動するとTestは失敗します。

しかしユーザーから見える結果が同じなら、Behavioral Regressionではありません。

実装詳細への依存が強いTestは、

壊れやすい
コードを変更するとすぐ壊れる

一方、

保証しない
本当のBugを見つける

能力が高いとは限りません。

Testing LibraryとPlaywrightがImplementation Detailではなく利用者から見えるBehaviorを重視している考え方は、AI生成Unit Testにも応用できます。

Testだけを読んで仕様を説明できる状態にする

良いTest Suiteなら、Implementationを開かなくても、

知りたいこと
このFunctionが何を保証しているのか

をある程度理解できます。

たとえば、

良いテスト名
it("5,000円未満なら送料550円", ...);
it("5,000円以上なら送料無料", ...);
it("負の購入金額は拒否する", ...);

なら、Test Nameを見るだけで仕様が分かります。

一方、

悪いテスト名
it("case 1", ...);
it("calculateShipping works", ...);

では何を保証しているのか分かりません。

AIへ、

指定例
テスト名には実装方法ではなく、
仕様上の入力条件と期待Behaviorを書いてください。

と指定すると、Test Suite自体を仕様書に近づけられます。

AIに「何をMockしないか」まで指定する

AIへUnit Testを依頼すると、依存関係をすべてMock化することがあります。

するとProductionでは、

連携する依存先
Database
Cache
Queue
外部API

が連携するのに、Testではほぼ何も本物が動いていない状態になります。

高速なUnit Testとしては有効でも、Integration Bugを見つけられません。

そこでTest Layerを分けます。

Business LogicだけはUnit Testで細かく確認します。

Database AccessはTest Databaseを使ったIntegration Testを用意します。

最重要FlowはPlaywrightなどでE2E Testします。

PlaywrightではBrowser ContextごとにTestを分離でき、再現性を高め、Test間で状態が連鎖することを防ぐ仕組みがあります。

すべてをMockしたUnit Testだけではなく、外部から動作を確認するLayerを残します。

AIにテストを書かせるプロンプト例

AIへテストを依頼するときは、次のようなPromptが使えます。

仕様起点の依頼例
この機能のテストをVitestで作成してください。

実装コードの現在の挙動を
正しい仕様だと仮定しないでください。

正しさの基準は以下の仕様です。

購入金額が5,000円以上なら送料0円。
5,000円未満なら送料550円。
負の購入金額は不正入力として拒否する。

実装と同じ条件式をテスト側へコピーしないでください。

まずテストケースを設計し、
境界値の直前、境界値、直後を含めてください。

正常系だけでなく、
不正入力も確認してください。

内部関数の呼び出し回数ではなく、
公開APIから観測できる結果を優先してください。

既存実装に不整合が見つかった場合は、
テストを実装に合わせず報告してください。

このPromptなら、

後付け
実装を読んでTestを後付け

するだけの場合より、仕様へ軸を戻しやすくなります。

さらに厳しくするならテストケースだけ先に生成する

より安全に進めるなら最初のPromptを、

設計のみ依頼
まだテストコードを書かないでください。

仕様から必要なテストケースを設計してください。

各ケースについて、
入力条件、
期待する外部Behavior、
どのBugを検出するためのケースなのかを説明してください。

実装コードの現在の挙動は
正しいと仮定しないでください。

とします。

人間がTest Caseを確認したあと、

実装依頼
承認したテストケースだけを
Vitestで実装してください。
期待値を変更しないでください。

と進めます。

AIに大規模リファクタリングを任せる方法で、

分離の考え方
Plan
↓
実装

に分けたのと同じ考え方です。

Testも、

分離する
Test Design
↓
Test Implementation

へ分離します。

AIがテストと実装を同時に変更したらDiffを分けて見る

AIコーディングAgentへ、

依頼例
Bugを直してTestも追加して

と依頼した場合は、Production CodeとTest Codeを分けて確認します。

最初に新しいTestを見ます。

そのTestが、

確認すべきこと
Bug修正前の実装では失敗するか

を確認します。

次に修正後の実装で成功するか確認します。

理想的なRegression Testは、

理想的なパターン
修正前: FAIL
修正後: PASS

になります。

修正前からPASSするTestなら、そのBugを再発防止するTestになっていません。

Regression TestではBugを再現してから修正する

AIへBug Fixを依頼するときは、

順番の違う依頼
先に修正してください

ではなく、

推奨される依頼
まず現在のBugを再現するFailing Testを追加してください。
まだProduction Codeは変更しないでください。

と依頼する方法があります。

たとえばIssueが、

Issue
5,000円ちょうどで送料が発生する

なら、まず、

shipping.test.ts
it(
  "5,000円ちょうどなら送料0円",
  () => {
    expect(
      calculateShipping(
        5000,
      ),
    ).toBe(0);
  },
);

を追加します。

現在のImplementationで、

FAIL
FAIL

になることを確認します。

その後で、

変更前
total > 5000

を、

変更後
total >= 5000

へ直します。

TestがPASSになれば、

保証されること
そのBugを検出できるTest

が確実に残ります。

Coverage ThresholdとMutation Testingを役割分担する

Coverageを捨てる必要はありません。

VitestではCoverage Thresholdを設定できるため、

vitest.config.ts
import {
  defineConfig,
} from "vitest/config";

export default defineConfig({
  test: {
    coverage: {
      thresholds: {
        lines: 90,
        functions: 90,
        branches: 85,
        statements: 90,
      },
    },
  },
});

のように最低CoverageをCIへ設定できます。

役割を分けると分かりやすくなります。

Coverageでは、

Coverageが探すもの
そもそも実行されていないコード

を探します。

Mutation Testingでは、

Mutation Testingが探すもの
実行されているが、
壊してもTestが気付かないコード

を探します。

StrykerではNo coverageSurvivedも別の状態として扱われています。

この2つを組み合わせると、AIが大量のTestを生成してCoverageだけ上げた状態を見抜きやすくなります。

全ファイルにMutation Testingする必要はない

Mutation Testingは通常のTest実行より計算量が増えます。

そのため最初からRepository全体へ適用する必要はありません。

特に重要な、

優先度高い領域
料金計算
権限判定
バリデーション
在庫
決済
日付計算
変換処理

などへ絞って導入できます。

AIが生成した複雑な条件分岐に対し、

使い方例
このTest SuiteでMutation Testingを行い、
生き残った条件分岐だけ調査する

という使い方もできます。

Mutation Scoreを100%にすること自体を目的にせず、重要なMutantがなぜ生き残ったかを見ることが重要です。

AIに大量のテストを作らせるより少数の強いテストを優先する

Test数が多いほど安全とは限りません。

たとえば、

冗長なテスト
20歳
21歳
22歳
23歳
24歳

について、

isAdult(age) === true
isAdult(age) === true

を100件Testしても、

見落としている値
18歳

を確認していなければ、

検出できないBug
age > 18

というBugを検出できません。

一方、

境界を確認できる例
17歳
18歳
19歳

の3Caseなら境界を確認できます。

AIに、

曖昧な依頼
できるだけ多くテストを書いて

と頼むより、

有効な依頼
各テストがどの種類のBugを検出するのか説明し、
重複するケースは減らしてください。

と依頼したほうが有用なTest Suiteになりやすくなります。

テストコードにもコードレビューが必要

Production CodeをAIに書かせたらReviewするのに、Test Codeは、

油断
テストだから安全

として確認しないことがあります。

しかしTest側にもBugは入ります。

たとえば、

緩いアサーション
expect(
  result,
).toBeTruthy();

で十分なのか、

具体的なアサーション
expect(
  result.status,
).toBe("completed");

まで確認すべきなのかでBug検出能力が変わります。

AIがMock Return Valueを間違えている場合もあります。

Setupで重要処理を飛ばしていることもあります。

Test CodeもProduction Codeと同じくReview対象です。

Codex公式Best Practicesでも、AIにコード変更とTestを行わせたあと、結果確認とReviewまで実施するWorkflowが推奨されています。

テスト実行だけでなく実際のアプリでも確認する

Test Suiteがすべて成功していても、ユーザーFlowを完全に再現できているとは限りません。

Claude Codeでは2026年時点で、TestやType Checkだけに頼らず実際にApplicationを起動して変更を確認する/run/verifyのSkillも提供されています。

Web Applicationなら、

多層検証
Unit Test
↓
Integration Test
↓
E2E
↓
実アプリ確認

という複数Layerで検証します。

AIが生成したTestを「正しさの唯一の証明」にしないことが重要です。

AI生成テストに関するよくある質問

QAIにテストを書かせても意味がないのですか

A意味はあります。問題はAIを使うことではなく、Implementationを唯一の正解としてTestを作らせることです。仕様、Acceptance Criteria、境界条件など、Implementationとは独立した正解を与えればAIによるTest生成を効率化できます。

Q実装とテストを同じAIに書かせてはいけませんか

A禁止する必要はありません。ただし同じ前提の間違いがImplementationとTestの両方へ入る可能性を考え、仕様から期待値を固定します。重要な機能ではTest設計を別SessionやReviewerへ分離する方法もあります。

QCoverage 100%なら十分ですか

ACoverageは重要ですが、それだけでは十分ではありません。VitestのCoverage ThresholdはLine、Function、Branch、Statementなどがどの程度実行されたかを管理できます。一方Mutation Testingでは、Production Codeへ小さな変更を入れてTestが検出できるかを確認できます。

QMutation Testingとは何ですか

AProduction Codeへ意図的な小さな変更を加え、その状態でTestを実行する方法です。Testが失敗すればMutantはkilled、成功したままならsurvivedです。StrykerではSurvived Mutantを、Test Suiteに不足している可能性のある箇所として確認できます。

QProperty-Based TestingはAI生成テストと相性がありますか

A相性があります。具体的な入力値だけでなく、InputとOutputの間で常に成立すべきPropertyを定義できるためです。fast-checkはJavaScript/TypeScript向けのProperty-Based Testing Libraryで、大量の入力を自動生成してPropertyを検証できます。

QAIにBug FixとTest追加を同時に任せてもよいですか

A可能ですが、Regression Testについては「修正前に失敗し、修正後に成功する」ことを確認したほうがよいでしょう。最初にFailing Testだけ追加させ、その後Production Codeを修正する方法なら、そのTestが本当にBugを再現していることを確認できます。

QUIテストでは何を基準にすればよいですか

A内部Component構造より、ユーザーが見るもの、操作するものを基準にします。Testing LibraryとPlaywrightはいずれも、Implementation Detailへの依存を避け、実際の利用方法に近いBehaviorを検証することを推奨しています。

まとめ

AIにテストを書かせること自体に問題があるわけではありません。

危険なのは、

危険な状態
Implementation
↓
AI
↓
Implementationを説明するTest

だけになっている状態です。

実装側が間違っていると、AIがその間違いを正しいBehaviorとしてTestへ固定する可能性があります。

特に、

注意すべき例
const expected =
  actualInput > 5000
    ? 0
    : 550;

のように、Production Codeと同じ条件式をTest側でも再実装している場合は注意します。

期待値はImplementationから計算するのではなく、仕様から固定します。

AIへテストを依頼するときは、最初にSpecification、Acceptance Criteria、境界条件、不正入力時のBehaviorを渡します。

可能であれば、

推奨する順序
仕様
↓
Test Case設計
↓
人間確認
↓
Test Code
↓
実装

という順序へ分けます。

UIでは内部ComponentやCSS Classではなく、ユーザーが実際に見るBehaviorをテストします。Testing LibraryとPlaywrightもImplementation Detailへの依存を避ける方針を採用しています。

入力パターンが多いロジックでは、fast-checkなどのProperty-Based Testingを使って、具体例だけでなく常に成立すべき性質を検証できます。

さらに重要なロジックではMutation Testingを実行します。

Production Codeへ意図的なBugを入れてもTestが成功するなら、そのTest Suiteにはまだ検出できない変更があります。

Strykerではこの状態をSurvived Mutantとして確認できます。

最終的には、

指標
Coverage

だけを見るのではなく、

重要な確認
実装を壊したらTestも壊れるか

を確認することが重要です。

AIコーディングではTest Codeを大量に生成できるため、今後は「何件のテストがあるか」より、そのテストがImplementationの間違いを本当に否定できるかが重要になります。

AIにテストを書かせるなら、実装を写させるのではなく、仕様を読ませ、境界を攻めさせ、最後にMutation Testingでそのテスト自体を疑うところまで行うと、AI生成テストを実用的な品質保証へ近づけられます。