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 regression")
やってはいけないこと:
- 一度もバグが発生したことのないコードに対してテストを作成する
- AIの自己レビューを自動テストの代替として信頼する
- 「単なるモックデータだから」とサンドボックスパスのテストをスキップする
- ユニットテストで十分な場合に統合テストを作成する
- カバレッジ率を目指す — 回帰防止を目指す