AI 回归测试¶
专为 AI 辅助开发设计的测试模式。当同一个模型编写代码并审查代码时,会产生只有自动化测试才能发现的系统性盲点。
启动时机¶
- 当 AI 智能体(Claude Code、Cursor、Codex)修改 API 路由或后端逻辑时
- 发现并修复了 Bug——需要防止其再次发生
- 项目中存在可用于无数据库测试的沙盒/模拟模式
- 在代码变更后运行
/bug-check或类似审查命令时 - 存在多个代码路径时(沙盒与生产、功能标志等)
核心问题¶
当 AI 编写代码,然后审查自己的工作时,它会在两个步骤中带入相同的前提。这会产生可预测的故障模式:
实际示例(在生产环境中观察到):
修正 1: API レスポンスに notification_settings を追加
→ SELECT クエリに追加するのを忘れた
→ AI がレビューして見逃した(同じブラインドスポット)
修正 2: SELECT クエリに追加
→ TypeScript ビルドエラー(生成された型に列がない)
→ AI が修正 1 をレビューしたが SELECT の問題を捕捉できなかった
修正 3: SELECT * に変更
→ 本番パスを修正、サンドボックスパスを忘れた
→ AI がレビューして再び見逃した(4 回目の発生)
修正 4: テストが最初の実行で即座に捕捉 PASS:
模式:沙盒/生产路径不一致是 AI 引入的回归问题中排名第一的。
沙盒模式 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 };
}
创建回归测试¶
重要原则:不要为能正常工作的代码编写测试,而要为你发现的 Bug 编写测试。
// __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);
}
}
});
});
将测试集成到 Bug 检查工作流¶
自定义命令定义¶
<!-- .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: 修正されたバグごとにリグレッションテストを提案する
工作流¶
ユーザー: "バグチェックして" (or "/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("Failed to load");
// reservations は前のタブのデータをまだ表示している!
}
// 成功: エラー時に関連する状態をクリアする
catch (err) {
setReservations([]); // 古いデータをクリア
setError("Failed to load");
}
模式 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 error");
} catch {
setItems(prevItems); // ロールバック
alert("削除に失敗しました");
}
};
策略:在发现 Bug 的地方进行测试¶
不要追求 100% 覆盖率。而是:
/api/user/profile でバグ発見 → プロファイル API のテストを書く
/api/user/messages でバグ発見 → メッセージ API のテストを書く
/api/user/favorites でバグ発見 → お気に入り API のテストを書く
/api/user/notifications でバグなし → テストを書かない(まだ)
为什么这在 AI 开发中有效:
- AI 倾向于重复同一类别的错误
- Bug 会聚集在复杂区域(认证、多路径逻辑、状态管理)
- 一旦测试覆盖,该精确回归无法再次发生
- 测试数量会随 Bug 修复而自然增长——没有无用功
快速参考¶
| AI 回归模式 | 测试策略 | 优先级 |
|---|---|---|
| 沙箱/生产环境不一致 | 在沙箱模式下断言相同的响应形状 | 高 |
| 省略 SELECT 子句 | 断言响应中的所有必需字段 | 高 |
| 错误状态泄漏 | 断言出错时的状态清理 | 中 |
| 缺少回滚 | 断言 API 失败时状态会被恢复 | 中 |
| 类型转换掩盖 null | 断言字段不为 undefined | 中 |
应该 / 不应该¶
应该: - 发现 Bug 后立即编写测试(如果可能,在修复之前) - 测试 API 响应形状,而不是实现 - 将所有 Bug 检查的第一步设为运行测试 - 保持测试快速(沙箱模式下总计 1 秒以内) - 用要防止的 Bug 为测试命名(例如:“BUG-R1 回归”)
不应该: - 为从未出现过 Bug 的代码编写测试 - 将 AI 自我审查作为自动化测试的替代 - 以“因为是模拟数据”为由跳过沙箱测试 - 在单元测试足够时编写集成测试 - 追求覆盖率百分比——应追求防止回归
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 ev/ECC