AI 回歸測試
專為 AI 輔助開發設計的測試模式,當同一個模型既編寫程式碼又審查程式碼時,會產生只有自動化測試才能捕捉的系統性盲點。
何時啟用
- AI 代理(Claude Code、Cursor、Codex)已修改 API 路由或後端邏輯
- 發現並修復了一個 Bug,需要防止它再次引入
- 專案擁有沙箱/模擬模式,可用於無資料庫測試
- 在程式碼變更後執行
/bug-check或類似的審查命令 - 存在多個程式碼路徑(沙箱 vs 生產環境、功能開關等)
核心問題
當 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:沙箱/生產環境路徑不匹配
頻率:最常見(在 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 檢查的第一步
- 保持測試快速(使用沙箱模式總時間 < 1 秒)
- 以它們要預防的 Bug 命名測試(例如「BUG-R1 回歸」)
不該做:
- 為從未有過 Bug 的程式碼撰寫測試
- 相信 AI 的自我審查可以替代自動化測試
- 因為「只是模擬資料」而跳過沙箱路徑測試
- 在單元測試就足夠時撰寫整合測試
- 追求覆蓋率百分比 — 目標應是防止回歸