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

テストにお悩みの方へ

😢開発リソースが足りない...
😢リリース直前だけどテストの余裕がない
😢開発コストを抑えたい

上記のようなお悩みに対して、テスト代行サービスを運営しています。まずは無料お問い合わせください。


目次

はじめに(誰向け?)

この記事は 業務開発で、AIにテスト生成・修正を手伝わせる人 向けです。
(例はPHP/Laravel(PHPUnit)で書きますが、考え方は言語・フレームワークを問いません。)

AIにコードを書かせると、テストも“それっぽく”量産できます。
でも本当に欲しいのは 「テストが通ること」 ではなく、「そのテストを信じて変更できること」 です。

AIは“最短でグリーン”に寄せるのが得意です。だからこそ、人間側が 「信じられるテストの型」 を先に決めておかないと、テストが逆にコストになります(壊れやすい/読めない/レビューできない)。

この記事では、AIにテストを書かせるときに効く 4つのルールを、実務目線でまとめます。最後に コピペ用テンプレチェックリスト も置きます。

(内部リンク:このテーマの前提として「テストでDRYをやりすぎると見通しが悪くなる」系の記事があれば、ここで1つ案内すると導線が作れます)


TL;DR(結論)

AIにテストを書かせるなら、まずこれだけ。

  1. DRYしすぎない:共通化で「何の仕様か」が消える
  2. コメントで合格ライン固定:スタート条件 / 依存条件 / 合格ライン
  3. モック地獄を避ける:呼び出し方より「結果」を見る
  4. 実装とテストを同時に大改造しない:「通すために弱める」を防ぐ

「信じられるテスト」の定義

ここで言う“信じられるテスト”は、だいたい次を満たします。

  • 読める:プロジェクト外の人でも意図が追える
  • 壊れにくい:リファクタで簡単に落ちない
  • 失敗が説明的:落ちたら「壊れた仕様」がすぐ分かる

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か(状態/外部出力/イベント/例外)

コピペ用テンプレ(テスト冒頭)

// スタート条件:
// 依存条件:
// 合格ライン:

運用のコツ(順番が大事)

  1. まずコメントだけを書く(合格ラインを言語化)
  2. そのコメントに沿ってテストコードを書く
  3. ズレたら コードより先にコメントを直す
  4. コメントとテストが揃ってから実装に入る

3. モック地獄を避ける(呼び出し方より“結果”)

AIは expects(...)->once() のような interaction(呼び出し検証) を好みがちです。
ただしこれは内部構造に密結合しやすく、リファクタで壊れやすい。

モック地獄のサイン

  • 回数・順序・引数の細部に依存している
  • 実装を少し整理しただけでテストが崩壊する
  • 落ちたとき「仕様が壊れた」のか「実装を変えただけ」なのか分からない

まずは「結果」を見る(優先順位)

  1. 永続化された状態(DB/Repo、ステータス遷移)
  2. 外に出た事実(メール内容、外部APIリクエスト、出力ファイル)
  3. 非同期に積まれた事実(イベント、キューpayload)
  4. レスポンス(HTTPステータス、表示文言)

原則

  • 原則:境界だけ置き換える(外部API、決済、メール、時刻、ファイルI/O)
  • 検証は「どう呼んだか」より 「何が起きたか」 に寄せる(Fakeが強い)

例外:interaction が“仕様”なとき

  • 二重送信防止(同じ請求を2回投げない)
  • 課金の一回性(課金APIは必ず1回だけ)
  • リトライ制御(失敗時に最大N回、など)

判断基準はこれだけ:
その検証が落ちたとき、「ユーザーにとって何が困るか」 を言える?

  • 言える → 仕様(残す)
  • 言えない → 実装都合(減らす)

4. 実装とテストを同時に大改造しない(合格ラインのすり替え防止)

AIに「直して」と言うと、実装とテストをまとめて編集して “通すためにテストを変える” 方向に流れがちです。ここが一番危ない。

おすすめ手順

  1. テスト(+コメント)で合格ラインを固定する
  2. 実装を変更する(テストが落ちてもOK)
  3. 落ちたら「仕様どおり落ちてるか」を確認する
  4. テストを変えるなら 理由を明記する
    • 仕様変更なのか
    • テストが過剰に実装に寄っていたのか
    • 期待値が誤りだったのか

レビューが楽になる小技

  • 「テスト修正」と「実装修正」を コミットで分ける
  • 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(回数/順序検証)が“仕様”として必要な範囲に収まっている
  • 外部境界以外をモックしすぎていない
  • テスト名だけで「守る仕様」がおおむね分かる
  • 実装とテストを同時に大改造していない
  • テスト変更の理由が説明されている(仕様変更か/過剰結合だったか/期待値ミスか)

参考リンク



ソフトウェアテスト代行サービスのご紹介

当社では10万円から始められるソフトウェアテスト代行サービスを提供しています。

テスト専門部隊による品質保証で、開発チームは本来の開発業務に集中できます。
品質向上と納期遵守の両立をサポートし、顧客からの信頼獲得に貢献します。

お問い合わせ

サービスに関するお問い合わせ、ご不明な点がございましたら、以下のお問い合わせフォームをご利用ください。お客様からのご質問に対し、担当者が責任を持ってお答えいたします。

よかったらシェアしてね!
目次
閉じる