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を中心に掘り下げます。
- AIにテストを書かせること自体は意味がある
- 最も危険なのは実装と期待値が同じ計算をしているテスト
- 期待値は実装ではなく仕様から固定する
- AIには実装より先に仕様を渡す
- 実装担当とテスト担当を分ける
- テスト名を先に作らせる
- 境界値を必ずテストさせる
- 正常系だけでなく失敗条件を仕様化する
- Private Methodを直接テストしすぎない
- Mockの呼び出し回数だけをテストしない
- UIテストではdata-testidだけに頼らない
- E2Eでは内部APIではなくユーザーの結果を見る
- テーブル駆動テストで仕様を固定する
- Golden Test Caseを人間が用意する
- Property-Based Testingで具体例から離れる
- Property-Based Testingでも実装の写経は避ける
- Code Coverageだけでテスト品質を判断しない
- Mutation Testingで「テストが本当に壊れるか」を確認する
- StrykerでAI生成テストを検査する
- AIにMutation Testingの結果を渡して改善させる
- 重要なBugをわざと入れてTestが落ちるか確認する
- Snapshot Testだけで安心しない
- Testが実装のFunction名に依存しすぎていないか確認する
- Testだけを読んで仕様を説明できる状態にする
- AIに「何をMockしないか」まで指定する
- AIにテストを書かせるプロンプト例
- さらに厳しくするならテストケースだけ先に生成する
- AIがテストと実装を同時に変更したらDiffを分けて見る
- Regression TestではBugを再現してから修正する
- Coverage ThresholdとMutation Testingを役割分担する
- 全ファイルにMutation Testingする必要はない
- AIに大量のテストを作らせるより少数の強いテストを優先する
- テストコードにもコードレビューが必要
- テスト実行だけでなく実際のアプリでも確認する
- AI生成テストに関するよくある質問
- まとめ
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が次のコードを生成しました。
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検出能力はほとんどありません。
期待値は実装ではなく仕様から固定する
先ほどのテストなら、期待値を計算する必要はありません。
仕様から直接書きます。
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
から、
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);
だけでは、
total > 5000
と、
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を確認します。
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でも同じ考え方を利用できます。
たとえば、
class PriceService {
private calculateTax() {
// ...
}
public getTotal() {
// ...
}
}
なら、PrivateなcalculateTax()を直接テストするより、
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にテストさせると、
<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を検証し、内部実装へ依存しないよう推奨しています。
たとえばログイン画面なら、
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()
というFunctionが呼ばれたことをE2E Testで確認する必要はありません。
重要なのは、ユーザーがログイン後の画面へ到達できたかです。
テーブル駆動テストで仕様を固定する
条件分岐が多い場合は、Test CaseをDataとして先に定義するとImplementationを写しにくくなります。
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へ期待値の決定まで任せない方法があります。
たとえば請求額、税金、ポイント、在庫、権限判定などです。
人間が先に、
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は入力と出力の間で常に成立すべき関係を定義し、多数の入力を生成して検証するものと説明されています。
たとえば割引率を適用する関数なら、
export function applyDiscount(
price: number,
rate: number,
): number {
return price *
(1 - rate);
}
について、
割引率が0〜1なら、 結果は0以上で元価格以下
という性質があります。
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が示すのは、
そのコードが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として扱います。
つまり、
Production Codeを壊した ↓ Testが失敗した
なら、そのTestにはBug検出能力があります。
逆に、
Production Codeを壊した ↓ 全Test成功
なら、Coverageが高くてもTest Suiteに弱い部分が残っています。
StrykerでAI生成テストを検査する
JavaScript/TypeScriptならStrykerJSを利用できます。
基本的な考え方は、
npx stryker run
でMutation Testingを実行し、生き残ったMutantを確認することです。
たとえば次の実装があるとします。
export function isAdult(
age: number,
): boolean {
return age >= 18;
}
AIが、
it(
"成人判定",
() => {
expect(
isAdult(20),
).toBe(true);
},
);
だけを書いた場合、
age >= 18
を、
age > 18
へ変えても20歳では結果が同じです。
Mutantが生き残る可能性があります。
そこで、
it(
"18歳は成人",
() => {
expect(
isAdult(18),
).toBe(true);
},
);
を追加します。
こうすると境界条件を変更したMutantを検出できます。
Mutation Testingは「Testが存在するか」ではなく、「Production Codeが間違ったときにTestが本当に失敗するか」を調べる方法です。StrykerもMutation Testingを「テストをテストする」ための仕組みとして説明しています。
AIにMutation Testingの結果を渡して改善させる
Mutation TestingもAIと組み合わせられます。
たとえばStrykerから、
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が変わった場合は、
なぜ変わったのか
を確認します。
ユーザーが見るべき重要な値については、
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で実装してください。 期待値を変更しないでください。
と進めます。
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が、
5,000円ちょうどで送料が発生する
なら、まず、
it(
"5,000円ちょうどなら送料0円",
() => {
expect(
calculateShipping(
5000,
),
).toBe(0);
},
);
を追加します。
現在のImplementationで、
FAIL
になることを確認します。
その後で、
total > 5000
を、
total >= 5000
へ直します。
TestがPASSになれば、
そのBugを検出できるTest
が確実に残ります。
Coverage ThresholdとMutation Testingを役割分担する
Coverageを捨てる必要はありません。
VitestではCoverage Thresholdを設定できるため、
import {
defineConfig,
} from "vitest/config";
export default defineConfig({
test: {
coverage: {
thresholds: {
lines: 90,
functions: 90,
branches: 85,
statements: 90,
},
},
},
});
のように最低CoverageをCIへ設定できます。
役割を分けると分かりやすくなります。
Coverageでは、
そもそも実行されていないコード
を探します。
Mutation Testingでは、
実行されているが、 壊してもTestが気付かないコード
を探します。
StrykerではNo coverageとSurvivedも別の状態として扱われています。
この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
を100件Testしても、
18歳
を確認していなければ、
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生成テストを実用的な品質保証へ近づけられます。
