ai-regression-testing

ai-regression-testing

热门

AI辅助开发的回归测试策略。无数据库依赖的沙盒模式API测试、自动化bug检查工作流,以及捕捉同一模型既写代码又审查代码时产生的AI盲点的模式。

23万Star
3.5万Fork
更新于 2026/7/17
SKILL.md
readonly只读
name
ai-regression-testing
description

AI辅助开发的回归测试策略。无数据库依赖的沙盒模式API测试、自动化bug检查工作流,以及捕捉同一模型既写代码又审查代码时产生的AI盲点的模式。

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);
      }
    }
  });
});

将测试集成到Bug检查工作流中

自定义命令定义

<!-- .claude/commands/bug-check.md -->
# Bug检查

## 步骤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检查会捕捉修复是否被破坏

常见AI回归模式

模式1:沙盒/生产路径不匹配

频率:最常见(在3/4的回归中观察到)

// 失败: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开发中有效:

  1. AI倾向于重复犯相同类别的错误
  2. Bug集中在复杂区域(认证、多路径逻辑、状态管理)
  3. 一旦测试,那个确切的回归不可能再次发生
  4. 测试数量随bug修复自然增长——没有浪费的努力

快速参考

AI回归模式 测试策略 优先级
沙盒/生产不匹配 在沙盒模式下断言相同的响应形状
SELECT子句遗漏 断言响应中包含所有必需字段
错误状态泄漏 断言出错时状态被清理
缺少回滚 断言API失败时状态被恢复
类型转换掩盖null 断言字段不为undefined

该做与不该做

该做:

  • 发现bug后立即编写测试(如果可能,在修复之前)
  • 测试API响应形状,而不是实现
  • 将测试作为每次bug检查的第一步
  • 保持测试快速(使用沙盒模式总时间<1秒)
  • 以测试防止的bug命名(例如,“BUG-R1回归”)

不该做:

  • 为从未出现过bug的代码编写测试
  • 相信AI自我审查可以替代自动化测试
  • 因为“只是模拟数据”而跳过沙盒路径测试
  • 在单元测试足够时编写集成测试
  • 追求覆盖率百分比——目标是防止回归