AI回帰テスト は何をしますか?
AI支援開発向けに特別に設計されたテストパターン。同じモデルがコードを書き、レビューすることで、自動テストだけが捕捉できる系統的な盲点が生じます。
アクティベーションのタイミング
- AIエージェント(Claude Code、Cursor、Codex)がAPIルートまたはバックエンドロジックを変更した場合
- バグを発見・修正した後で、再発を防止したい場合
- データベース不要のテストに活用できるサンドボックス/モックモードがプロジェクトにある場合
- コード変更後に
/bug-checkまたは同様のレビューコマンドを実行する場合 - 複数のコードパスが存在する場合(サンドボックス vs プロダクション、フィーチャーフラグなど)
核心的な問題
AIがコードを書き、その自分のコードをレビューするとき、両方のステップで同じ前提を持ち込みます。これにより、予測可能な障害パターンが発生します。
AIが修正を書く → AIが修正をレビューする → AIは「正しい」と言う → バグは依然として存在する
実際の例(本番環境で観測):
修正1: notification_settingsをAPIレスポンスに追加
→ SELECTクエリに追加し忘れた
→ AIがレビューしたが見逃した(同じ盲点)
修正2: SELECTクエリに追加
→ TypeScriptビルドエラー(生成された型にカラムがない)
→ AIは修正1をレビューしたが、SELECTの問題に気づかなかった
修正3: SELECT * に変更
→ プロダクションパスは修正したが、サンドボックスパスを忘れた
→ AIが再びレビューしたが見逃した(4回目)
修正4: テストが初回実行で即座に捕捉 PASS:
パターン: サンドボックス/プロダクションパスの不一致 は、AIが引き起こす回帰(リグレッション)の第1位です。
サンドボックスモードAPIテスト
AIフレンドリーなアーキテクチャを持つほとんどのプロジェクトには、サンドボックス/モックモードがあります。これがデータベース不要の高速APIテストの鍵です。
セットアップ(Vitest + Next.js App Router)
// vitest.config.ts
import { defineConfig } from "vitest/config";
import path from "path";
export default defineConfig({
test: {
environment: "node",
globals: true,
include: ["__tests__/**/*.test.ts"],
setupFiles: ["__tests__/setup.ts"],
},
resolve: {
alias: {
"@": path.resolve(__dirname, "."),
},
},
});
// __tests__/setup.ts
// サンドボックスモードを強制 — データベース不要
process.env.SANDBOX_MODE = "true";
process.env.NEXT_PUBLIC_SUPABASE_URL = "";
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = "";
Next.js APIルート用テストヘルパー
// __tests__/helpers.ts
import { NextRequest } from "next/server";
export function createTestRequest(
url: string,
options?: {
method?: string;
body?: Record<string, unknown>;
headers?: Record<string, string>;
sandboxUserId?: string;
},
): NextRequest {
const { method = "GET", body, headers = {}, sandboxUserId } = options || {};
const fullUrl = url.startsWith("http") ? url : `http://localhost:3000${url}`;
const reqHeaders: Record<string, string> = { ...headers };
if (sandboxUserId) {
reqHeaders["x-sandbox-user-id"] = sandboxUserId;
}
const init: { method: string; headers: Record<string, string>; body?: string } = {
method,
headers: reqHeaders,
};
if (body) {
init.body = JSON.stringify(body);
reqHeaders["content-type"] = "application/json";
}
return new NextRequest(fullUrl, init);
}
export async function parseResponse(response: Response) {
const json = await response.json();
return { status: response.status, json };
}
回帰テストの記述
重要な原則: 動作しているコードではなく、見つかったバグに対してテストを書く。
// __tests__/api/user/profile.test.ts
import { describe, it, expect } from "vitest";
import { createTestRequest, parseResponse } from "../../helpers";
import { GET, PATCH } from "@/app/api/user/profile/route";
// コントラクトを定義 — レスポンスに必須のフィールド
const REQUIRED_FIELDS = [
"id",
"email",
"full_name",
"phone",
"role",
"created_at",
"avatar_url",
"notification_settings", // ← バグで欠落していたため追加
];
describe("GET /api/user/profile", () => {
it("必須フィールドをすべて返す", async () => {
const req = createTestRequest("/api/user/profile");
const res = await GET(req);
const { status, json } = await parseResponse(res);
expect(status).toBe(200);
for (const field of REQUIRED_FIELDS) {
expect(json.data).toHaveProperty(field);
}
});
// 回帰テスト — このバグはAIによって4回も導入された
it("notification_settingsがundefinedでないこと(BUG-R1回帰)", async () => {
const req = createTestRequest("/api/user/profile");
const res = await GET(req);
const { json } = await parseResponse(res);
expect("notification_settings" in json.data).toBe(true);
const ns = json.data.notification_settings;
expect(ns === null || typeof ns === "object").toBe(true);
});
});
サンドボックス/プロダクションの同等性テスト
最も一般的なAI回帰: プロダクションパスを修正したがサンドボックスパスを忘れる(またはその逆)。
// サンドボックスレスポンスが期待されるコントラクトと一致することをテスト
describe("GET /api/user/messages(会話リスト)", () => {
it("サンドボックスモードでpartner_nameを含むこと", async () => {
const req = createTestRequest("/api/user/messages", {
sandboxUserId: "user-001",
});
const res = await GET(req);
const { json } = await parseResponse(res);
// これにより、partner_nameがプロダクションパスに追加されたが
// サンドボックスパスには追加されなかったバグを捕捉
if (json.data.length > 0) {
for (const conv of json.data) {
expect("partner_name" in conv).toBe(true);
}
}
});
});
バグチェックワークフローへのテスト統合
カスタムコマンド定義
<!-- .claude/commands/bug-check.md -->
# バグチェック
## ステップ1: 自動テスト(必須、スキップ不可)
コードレビューを行う前に、まず以下のコマンドを実行してください:
npm run test # Vitestテストスイート
npm run build # TypeScript型チェック + ビルド
- テストが失敗した場合 → 最優先のバグとして報告
- ビルドが失敗した場合 → 型エラーを最優先として報告
- 両方が成功した場合のみ、ステップ2に進む
## ステップ2: コードレビュー(AIレビュー)
1. サンドボックス/プロダクションパスの一貫性
2. APIレスポンスの形状がフロントエンドの期待と一致しているか
3. SELECT句の完全性
4. ロールバックを含むエラーハンドリング
5. 楽観的更新のレースコンディション
## ステップ3: 修正された各バグについて、回帰テストを提案
ワークフロー
ユーザー: "バグチェックして" (または "/bug-check")
│
├─ ステップ1: npm run test
│ ├─ FAIL → バグを機械的に検出(AIの判断不要)
│ └─ PASS → 続行
│
├─ ステップ2: npm run build
│ ├─ FAIL → 型エラーを機械的に検出
│ └─ PASS → 続行
│
├─ ステップ3: AIコードレビュー(既知の盲点に注意)
│ └─ 結果を報告
│
└─ ステップ4: 各修正に対して回帰テストを記述
└─ 次回のバグチェックで修正の破損を検出
一般的なAI回帰パターン
パターン1: サンドボックス/プロダクションパスの不一致
頻度: 最も多い(4件中3件の回帰で観測)
// 失敗: AIがプロダクションパスにのみフィールドを追加
if (isSandboxMode()) {
return { data: { id, email, name } }; // 新しいフィールドが欠落
}
// プロダクションパス
return { data: { id, email, name, notification_settings } };
// 成功: 両方のパスが同じ形状を返すこと
if (isSandboxMode()) {
return { data: { id, email, name, notification_settings: null } };
}
return { data: { id, email, name, notification_settings } };
それを捕捉するテスト:
it("サンドボックスとプロダクションが同じフィールドを返すこと", async () => {
// テスト環境では、サンドボックスモードが強制ON
const res = await GET(createTestRequest("/api/user/profile"));
const { json } = await parseResponse(res);
for (const field of REQUIRED_FIELDS) {
expect(json.data).toHaveProperty(field);
}
});
パターン2: SELECT句の欠落
頻度: Supabase/Prismaで新しいカラム追加時に多い
// 失敗: 新しいカラムがレスポンスに追加されたが、SELECTには含まれていない
const { data } = await supabase
.from("users")
.select("id, email, name") // notification_settingsがここにない
.single();
return { data: { ...data, notification_settings: data.notification_settings } };
// → notification_settingsは常にundefined
// 成功: SELECT * を使用するか、新しいカラムを明示的に含める
const { data } = await supabase
.from("users")
.select("*")
.single();
パターン3: エラー状態のリーク
頻度: 中程度 — 既存コンポーネントにエラーハンドリングを追加する場合
// 失敗: エラー状態は設定されたが、古いデータがクリアされていない
catch (err) {
setError("読み込みに失敗しました");
// reservationsには前のタブのデータがまだ表示されている!
}
// 成功: エラー時に関連する状態をクリア
catch (err) {
setReservations([]); // 古いデータをクリア
setError("読み込みに失敗しました");
}
パターン4: 適切なロールバックのない楽観的更新
// 失敗: 失敗時のロールバックなし
const handleRemove = async (id: string) => {
setItems(prev => prev.filter(i => i.id !== id));
await fetch(`/api/items/${id}`, { method: "DELETE" });
// APIが失敗した場合、アイテムはUIから消えるがDBには残る
};
// 成功: 以前の状態をキャプチャし、失敗時にロールバック
const handleRemove = async (id: string) => {
const prevItems = [...items];
setItems(prev => prev.filter(i => i.id !== id));
try {
const res = await fetch(`/api/items/${id}`, { method: "DELETE" });
if (!res.ok) throw new Error("APIエラー");
} catch {
setItems(prevItems); // ロールバック
alert("削除に失敗しました");
}
};
戦略: バグが見つかった場所にテストを書く
100%カバレッジを目指さないでください。代わりに:
/api/user/profile でバグが見つかった → profile APIのテストを書く
/api/user/messages でバグが見つかった → messages APIのテストを書く
/api/user/favorites でバグが見つかった → favorites APIのテストを書く
/api/user/notifications ではバグなし → テストを書かない(まだ)
これがAI開発で有効な理由:
- AIは同じ種類のミスを繰り返す傾向がある
- バグは複雑な領域(認証、マルチパスロジック、状態管理)に集中する
- 一度テストされれば、その正確な回帰は二度と発生しない
- テスト数はバグ修正とともに有機的に増加 — 無駄な労力なし
クイックリファレンス
| AI回帰パターン | テスト戦略 | 優先度 |
|---|---|---|
| サンドボックス/プロダクションの不一致 | サンドボックスモードで同じレスポンス形状をアサート | 高 |
| SELECT句の欠落 | レスポンス内のすべての必須フィールドをアサート | 高 |
| エラー状態のリーク | エラー時の状態クリアをアサート | 中 |
| ロールバックの欠落 | API失敗時の状態復元をアサート | 中 |
| 型キャストによるnullのマスキング | フィールドがundefinedでないことをアサート | 中 |
すべきこと / すべきでないこと
すべきこと:
- バグを見つけたらすぐにテストを書く(可能なら修正する前に)
- 実装ではなく、APIレスポンスの形状をテストする
- すべてのバグチェックの最初のステップとしてテストを実行する
- テストを高速に保つ(サンドボックスモードで合計1秒未満)
- テスト名を、それが防ぐバグにちなんで命名する(例: "BUG-R1回帰")
すべきでないこと:
- 一度もバグが発生したことのないコードにテストを書く
- AIの自己レビューを自動テストの代わりとして信頼する
- 「単なるモックデータだから」とサンドボックスパスのテストをスキップする
- 単体テストで十分な場合に結合テストを書く
- カバレッジ率を目指す — 回帰防止を目指す