AI時代のテスト戦略:通るより「信じられるテスト」へ

はじめに(誰向け?)
この記事は 業務開発で、AIにテスト生成・修正を手伝わせる人 向けです。
(例はPHP/Laravel(PHPUnit)で書きますが、考え方は言語・フレームワークを問いません。)
AIにコードを書かせると、テストも“それっぽく”量産できます。
でも本当に欲しいのは 「テストが通ること」 ではなく、「そのテストを信じて変更できること」 です。
AIは“最短でグリーン”に寄せるのが得意です。だからこそ、人間側が 「信じられるテストの型」 を先に決めておかないと、テストが逆にコストになります(壊れやすい/読めない/レビューできない)。
この記事では、AIにテストを書かせるときに効く 4つのルールを、実務目線でまとめます。最後に コピペ用テンプレ と チェックリスト も置きます。
(内部リンク:このテーマの前提として「テストでDRYをやりすぎると見通しが悪くなる」系の記事があれば、ここで1つ案内すると導線が作れます)
TL;DR(結論)

AIにテストを書かせるなら、まずこれだけ。
- DRYしすぎない:共通化で「何の仕様か」が消える
- コメントで合格ライン固定:スタート条件 / 依存条件 / 合格ライン
- モック地獄を避ける:呼び出し方より「結果」を見る
- 実装とテストを同時に大改造しない:「通すために弱める」を防ぐ
「信じられるテスト」の定義
ここで言う“信じられるテスト”は、だいたい次を満たします。
- 読める:プロジェクト外の人でも意図が追える
- 壊れにくい:リファクタで簡単に落ちない
- 失敗が説明的:落ちたら「壊れた仕様」がすぐ分かる
AI時代に大事なのは「テストの量」より、この3つを満たす割合です。
0. 用語(本稿ではこう呼ぶ)

厳密な定義はさておき、この記事ではこう扱います。
- Mock(モック):呼び出し回数・順序・引数など「どう呼んだか」を期待として設定して検証するもの
- Fake(フェイク):メール送信やRepoなどを“簡易実装”で置き換え、「何が起きたか(結果)」を検証しやすくするもの
- Stub(スタブ):固定値を返すだけの差し替え(時刻やトークン生成など)
1. DRYしすぎない(共通化で「何の仕様か」が消える)
プロダクトコードではDRYが効きます。
一方テストは “実行できる仕様書” として読まれるので、共通化しすぎると「何の仕様か」が見えなくなります。
ここはDRYより、DAMP(Descriptive And Meaningful Phrases:説明的で意味のある記述) 寄りのほうが、テストの価値が上がることが多いです。
参考:Google Testing Blog “Tests Too DRY? Make Them DAMP!”
AIがやりがちな事故
setup()/ helper に前提が吸い込まれて Given(前提)が読めない- assertが共通化されて 保証している内容が薄まる
- 1テストに観点が詰まり、失敗しても原因が絞れない
ルール
- 迷ったら コピペで冗長に書く(テストは冗長でいい)
- 共通化するなら “意図が残る共通化”だけ
- 良い例:
createUser(role: 'admin')(呼んだ瞬間に意味が分かる) - ダメ例:
prepare()(開かないと意味が分からない)
- 良い例:
悪い例 → 良い例(DRYしすぎ)
悪い例(Givenが隠れる)
// 何が前提なのか、prepare()を開かないと分からない
[$usecase, $repo, $mailer] = $this->prepare();
$usecase->invite(...);
$this->assertOk($repo, $mailer);
良い例(1画面で仕様が見える)
$repo = new InMemoryInvitationRepo();
$mailer = new FakeMailer();
$tokenGen = fn() => 'INV-2025-12-18-000042';
$usecase = new InviteUser($repo, $mailer, $tokenGen);
$usecase->invite(inviterId: 1001, inviterRole: 'admin', email: 'taro.yamada@acme.test');
// 永続化・外部出力という「結果」を検証
$this->assertNotNull($repo->findByEmail('taro.yamada@acme.test'));
$this->assertSame('taro.yamada@acme.test', $mailer->last()['to']);
目標
- Given / When / Then が1画面で追える(最低限ここ)
(内部リンク:テストの“読みやすい命名・構造テンプレ”記事があるなら、ここで案内)
2. コメントで合格ラインを固定する(テストは実行できる仕様書)
AIにテストを書かせるときは、コードより先に 意図(仕様)を固定したい。
一番手堅いのが、テスト冒頭コメントを“仕様”として扱うことです。
コメントはこの3点セット
- スタート条件:権限・時刻・設定・ログイン状態など
- 依存条件:DB初期データ、外部API契約、固定入力など
- 合格ライン:何が満たされたらOKか(状態/外部出力/イベント/例外)
コピペ用テンプレ(テスト冒頭)
// スタート条件:
// 依存条件:
// 合格ライン:
運用のコツ(順番が大事)
- まずコメントだけを書く(合格ラインを言語化)
- そのコメントに沿ってテストコードを書く
- ズレたら コードより先にコメントを直す
- コメントとテストが揃ってから実装に入る
3. モック地獄を避ける(呼び出し方より“結果”)
AIは expects(...)->once() のような interaction(呼び出し検証) を好みがちです。
ただしこれは内部構造に密結合しやすく、リファクタで壊れやすい。
モック地獄のサイン
- 回数・順序・引数の細部に依存している
- 実装を少し整理しただけでテストが崩壊する
- 落ちたとき「仕様が壊れた」のか「実装を変えただけ」なのか分からない
まずは「結果」を見る(優先順位)
- 永続化された状態(DB/Repo、ステータス遷移)
- 外に出た事実(メール内容、外部APIリクエスト、出力ファイル)
- 非同期に積まれた事実(イベント、キューpayload)
- レスポンス(HTTPステータス、表示文言)
原則
- 原則:境界だけ置き換える(外部API、決済、メール、時刻、ファイルI/O)
- 検証は「どう呼んだか」より 「何が起きたか」 に寄せる(Fakeが強い)
例外:interaction が“仕様”なとき
- 二重送信防止(同じ請求を2回投げない)
- 課金の一回性(課金APIは必ず1回だけ)
- リトライ制御(失敗時に最大N回、など)
判断基準はこれだけ:
その検証が落ちたとき、「ユーザーにとって何が困るか」 を言える?
- 言える → 仕様(残す)
- 言えない → 実装都合(減らす)
4. 実装とテストを同時に大改造しない(合格ラインのすり替え防止)
AIに「直して」と言うと、実装とテストをまとめて編集して “通すためにテストを変える” 方向に流れがちです。ここが一番危ない。
おすすめ手順
- テスト(+コメント)で合格ラインを固定する
- 実装を変更する(テストが落ちてもOK)
- 落ちたら「仕様どおり落ちてるか」を確認する
- テストを変えるなら 理由を明記する
- 仕様変更なのか
- テストが過剰に実装に寄っていたのか
- 期待値が誤りだったのか
レビューが楽になる小技
- 「テスト修正」と「実装修正」を コミットで分ける
- PR説明も「合格ライン」「実装変更」「テスト変更理由」を段落で分ける
(内部リンク:コミット分割・PR説明テンプレの記事があるなら、ここに置くと強い)
チーム運用に落とす:CLAUDE.md / AGENTS.md
私は下記の「テスト運用ルール」を AGENTS.md に書いています。
同様に、テストのルールを CLAUDE.md / AGENTS.md に書いておくことを推奨します(下はコピペ用です)。
※AIモデルやチームによって最適解は変わります。この内容は参考程度にして、運用で出た学びを AGENTS.md / CLAUDE.md に反映して育ててください。
**テスト運用(“無理やり通す”禁止)**
- テストは「仕様/期待するふるまい」を表す。既存実装が怪しい/壊れている可能性がある場面で、期待値変更・雑なモック・skip等で“緑化”しない。
- テスト失敗の原因が「仕様不明 / 既存実装の不具合 / データ不整合」の可能性がある場合、独断で進めず、必ず承諾を得る。
- テストを書いている(テスト追加/修正している)最中は、原則として本体コード側の変更で帳尻を合わせない。もし本体コード修正が必要なら、必ず承諾を得てから実施する(理由/影響範囲/選択肢/推奨案を提示)。
- 外部API(HTTP/SDK等)を含むテストで、モック/スタブ/フェイクを導入して検証する場合は、必ず承諾を得てから実施する(何をモックし、何を確認するかを提示)。
- 外部APIへの実通信をテストで行わない(原則)。実通信が必要な場合は、必ず承諾を得たうえで、対象・回数・実行環境・失敗時の扱いを明確にする。
- 例外(要承諾): **現状挙動の凍結(回帰テスト)**。直せない/仕様確定待ち等で「今の挙動を一旦固定して事故を防ぐ」目的のテストを追加してよいが、必ずテスト内の日本語コメントで次を明記する。
- 既知の怪しさ(何が問題っぽいか)
- 本来の期待(想定仕様)
- 今回は凍結する理由(影響/期限/範囲)
付録(ここからコピペ用)
付録A:PHPUnit例(コメント → Given/When/Then → 結果を検証)
呼び出し回数より、仕様としての結果(保存されたもの/外に出たもの)を確認する例です。
※依存は Interface に寄せ、テストでは Fake/InMemory を差し替えています。
<?php
use PHPUnit\Framework\TestCase;
final class InviteUserTest extends TestCase
{
public function test_admin_can_invite_user_and_invitation_is_persisted_and_mail_is_prepared(): void
{
// スタート条件:inviterはadmin / tokenは固定
// 依存条件:RepoはInMemory / MailerはFake
// 合格ライン:招待が保存され、メール(宛先・token)が期待通り
// Given(前提)
$repo = new InMemoryInvitationRepo(); // 永続化の受け皿(DBの代わり)
$mailer = new FakeMailer(); // 外部I/O(メール)は実送信しない
$tokenGen = fn() => 'INV-2025-12-18-000042'; // ランダム要素は固定化
$usecase = new InviteUser($repo, $mailer, $tokenGen);
$inviterId = 1001;
$inviterRole = 'admin';
$inviteeEmail = 'taro.yamada@acme.test';
// When(操作)
$usecase->invite(inviterId: $inviterId, inviterRole: $inviterRole, email: $inviteeEmail);
// Then(結果)
// 1) 永続化された結果(仕様として強い)
$inv = $repo->findByEmail($inviteeEmail);
$this->assertNotNull($inv, '招待が保存されていること');
$this->assertSame(1001, $inv['inviter_id'], 'inviter_id が期待通りであること');
$this->assertSame('taro.yamada@acme.test', $inv['email'], 'email が期待通りであること');
$this->assertSame('INV-2025-12-18-000042', $inv['token'], 'token が期待通りであること');
// 2) 外に出た事実(メール内容)
$mail = $mailer->last();
$this->assertSame('taro.yamada@acme.test', $mail['to'], 'メール宛先が期待通りであること');
$this->assertSame('INV-2025-12-18-000042', $mail['token'], 'メールtokenが期待通りであること');
}
}
付録B:AIに渡す指示テンプレ(コピペ用)
1) 新規テスト生成
あなたは「プロジェクト外のジュニアでも読める」テストを書く。必ず守ること。
- テストはDRYしすぎない。共通化で意図が消えるならコピペで冗長に書く
- テスト冒頭に仕様コメントを書く(スタート条件/依存条件/合格ライン)
- interaction(回数/順序)は最小限。基本は結果(状態/外部出力/イベント)を検証する
- モック/スタブは境界(外部API、メール、決済、時刻、ファイルI/O)に限定する
- テスト名は「状況 + 操作 + 期待結果」が分かる形にする
手順:
(1) まず仕様コメントだけを書く
(2) 次にGiven/When/Thenでテストコードを書く
2) 既存修正(暴走防止)
実装とテストを同時に大改造しない。
フェーズ1:テストの合格ライン(冒頭コメント)を確認し、必要ならコメントだけを明確化する(この時点で実装は変えない)
フェーズ2:実装のみ変更し、合格ラインを満たすようにする
フェーズ3:テスト修正が必要なら最小限にし、「なぜテストを変えたか」をコメントに追記する
テストを通すために期待値を弱めたり、検証を削ったりしない。
付録C:レビュー用チェックリスト(Yes/No)
- Given/When/Then が1画面で追える
- helperを開かないと前提が分からない、になっていない
- テスト冒頭コメントに「スタート条件/依存条件/合格ライン」がある
- 合格ラインが具体的(何が起きたらOKか言える)
- 1テストで観点を詰め込みすぎていない(失敗原因が絞れる)
- interaction(回数/順序検証)が“仕様”として必要な範囲に収まっている
- 外部境界以外をモックしすぎていない
- テスト名だけで「守る仕様」がおおむね分かる
- 実装とテストを同時に大改造していない
- テスト変更の理由が説明されている(仕様変更か/過剰結合だったか/期待値ミスか)
参考リンク
