AI回归测试 是做什么的?
专为AI辅助开发设计的测试模式,解决同一模型既编写代码又审查代码时产生的系统性盲点——只有自动化测试才能捕获这些盲点。
何时激活
- AI代理(Claude Code、Cursor、Codex)已修改API路由或后端逻辑
- 发现并修复了一个bug——需要防止重新引入
- 项目有沙箱/模拟模式,可用于无数据库测试
- 在代码更改后运行
/bug-check或类似的审查命令 - 存在多个代码路径(沙箱与生产、功能开关等)
核心问题
当AI编写代码然后审查自己的代码时,它会在两个步骤中带入相同的假设。这会产生一个可预测的失败模式:
AI写入修复 → AI审查修复 → AI说“看起来正确” → Bug仍然存在
真实示例(在生产中观察到):
修复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", // ← 在发现bug缺失后添加
];
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);
}
});
// 回归测试——这个确切的bug被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);
// 这捕获了一个bug,其中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 -->
# Bug Check
## 步骤1:自动化测试(强制,不可跳过)
在进行任何代码审查之前,先运行以下命令:
npm run test # Vitest测试套件
npm run build # TypeScript类型检查 + 构建
- 如果测试失败→报告为最高优先级bug
- 如果构建失败→将类型错误报告为最高优先级
- 仅当两者都通过后才进入步骤2
## 步骤2:代码审查(AI审查)
1. 沙箱/生产路径一致性
2. API响应形状与前端的期望一致
3. SELECT子句完整性
4. 带回滚的错误处理
5. 乐观更新竞态条件
## 步骤3:对于每个修复的bug,建议一个回归测试
工作流
用户: "バグチェックして" (或 "/bug-check")
│
├─ 步骤1: npm run test
│ ├─ 失败 → 机械地发现bug(无需AI判断)
│ └─ 通过 → 继续
│
├─ 步骤2: npm run build
│ ├─ 失败 → 机械地发现类型错误
│ └─ 通过 → 继续
│
├─ 步骤3: AI代码审查(牢记已知盲点)
│ └─ 报告发现的问题
│
└─ 步骤4: 对于每个修复,编写一个回归测试
└─ 下一次bug-check将捕获修复是否被破坏
常见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 () => {
// 在测试环境中,沙箱模式被强制开启
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消失但仍在数据库中
};
// 通过:捕获先前状态并在失败时回滚
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("删除失败");
}
};
策略:在发现bug的地方测试
不要追求100%的覆盖率。相反:
在/api/user/profile中发现bug → 为profile API编写测试
在/api/user/messages中发现bug → 为messages API编写测试
在/api/user/favorites中发现bug → 为favorites API编写测试
/api/user/notifications中没有bug → 不要编写测试(暂缓)
为什么这适用于AI开发:
- AI倾向于重复犯同类型的错误
- Bug集中在复杂区域(身份验证、多路径逻辑、状态管理)
- 一旦测试过,那种确切的回归不可能再次发生
- 测试数量随着bug修复而有机增长——无浪费的工作
快速参考
| AI回归模式 | 测试策略 | 优先级 |
|---|---|---|
| 沙箱/生产不匹配 | 在沙箱模式下断言相同的响应形状 | 高 |
| SELECT子句遗漏 | 断言响应中包含所有必需字段 | 高 |
| 错误状态泄漏 | 断言错误时状态已清理 | 中 |
| 缺少回滚 | 断言API失败时状态已恢复 | 中 |
| 类型转换掩盖null | 断言字段不是undefined | 中 |
该做与不该做
该做:
- 发现bug后立即编写测试(如果可能,在修复之前)
- 测试API响应形状,而不是实现
- 将测试作为每次bug-check的第一步运行
- 保持测试快速(沙箱模式下总计< 1秒)
- 以测试防止的bug命名测试(例如,“BUG-R1回归”)
不该做:
- 为从未有过bug的代码编写测试
- 相信AI自我审查可以替代自动化测试
- 因为“它只是模拟数据”而跳过沙箱路径测试
- 在单元测试就足够时编写集成测试
- 追求覆盖率百分比——目标是防止回归