AI 輔助測試:用單元測試門禁兜住 AI 生成代碼的底

分享 AI 輔助測試的完整工作流, 從設計文檔到用例代碼, 用單元測試門禁兜住 AI 生成代碼的風險, 把檢視對象從代碼換成用例文檔, 讓失敗用例替人盯住 AI 的偷懶與出錯, 讓開發對 vibe coding 心裡有底.

AI 輔助測試解決的不是測試覆蓋率, 而是我對 AI 生成代碼的信任問題. AI 三分鐘能吐出一大段代碼, 人卻要兩小時才檢視得完, 三分鐘生成, 兩小時檢視並不誇張. 我的解法是換一個檢視對象: 不去逐行讀代碼, 改去讀用例文檔, 再用單元測試在本地開發環境建一道門禁, 讓失敗的用例替我盯住 AI 的偷懶和出錯.

小 bug 才是 AI 輔助測試的主戰場

bug 分為兩類: 好解的 bug, 和不好解的 bug.

不好解的 bug 通常源於架構或方案設計缺陷, 逐步演化為歷史包袱, 牽一發而動全身; 也可能由第三方引起, 如三方庫, 作業系統, 編譯器, 需要更專業的知識和深入探索才能解決. 這類複雜問題不在本文探討範圍.

本文關注的是產品生命週期裡的小 bug, 借助 AI 輔助建立測試用例, 解決以下問題:

  • 可以快速發現的 bug
  • 只需極小改動就能修復的低級 bug
  • 修改引入的新 bug
  • 緊急修改時忙中出錯
  • 針對性測試的成本與效率權衡

大多數 bug 是小 bug, 但小 bug 也可能造成嚴重問題. 把小 bug 解決在本地, 能省下大量後續精力.

把檢視對象從代碼換成用例文檔

我曾嘗試過一個最大膽的想法: 讓 AI 全權完成一個需求, 完全不檢視它生成的代碼, 類似 Copilot Agent 模式 那樣徹底放手. 後來放棄了. 根本原因是心裡沒底. 對於一個邏輯嚴謹, 0 1 分明的二進制程式, 心裡沒底就意味著丟掉上下游的信任, 丟掉客戶的信任. 所以我在學習對 vibe coding 的思考: 對 AI 生成的代碼, 一定要心裡有底.

問題在於檢視成本. AI 有時生成或修改的代碼量太大, 三分鐘生成, 兩小時檢視並不誇張. 測試代碼的工作量通常還多於業務代碼, 且未必計入研發的交付考核, 這是很多人不願主動寫測試的原因之一. 但 AI 助手比我聰明很多 已是事實, 與其抗拒, 不如給它配一道門禁.

把檢視對象從代碼換成用例文檔, 是繞開這個死結的關鍵. 與業務代碼不同, AI 生成的測試用例通常描述一個非常具體的場景, 易於檢視和驗證. 很多用例甚至不必逐行讀, 只等它報錯時再細看. 说完全不檢視 AI 代碼是言過其實, 但借助完善的用例文檔, 可以證明並非每行代碼都需要人工 review.

---
title: 從檢視代碼到檢視文檔
---
flowchart TB
    A[AI 生成大量代碼] e1@--> B[逐行檢視成本高]
    B e2@--> C{檢視什麼}
    C e3@-->|代碼| D[兩小時檢視, 心裡沒底]
    C e4@-->|用例文檔| E[易於檢視, 快速驗證]
    E e5@--> F[單元測試門禁]
    F e6@--> G[失敗用例暴露問題]

    classDef risk fill:#FFF3E0,stroke:#E65100,color:#E65100;
    classDef ok fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20;
    classDef animate stroke:#EF6C00,stroke-width:2px,stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
    class D risk;
    class E,F,G ok;
    class e1,e2,e3,e4,e5,e6 animate;

方法論底座: 六西格瑪與 DMAIC

Six Sigma 是以數據驅動的品質改進方法論, 核心目標是透過減少過程變異, 把缺陷率控制在百萬分之 3.4, 即 6σ 水平1.

它給出了一套可執行的流程 DMAIC:

  • Define 定義: 需求分析, 轉化為數據模型, 設計數據結構和介面
  • Measure 測量: 用例監控, 覆蓋率, 性能, 安全
  • Analyze 分析: 分析用例和業務代碼
  • Improve 改進: 改進用例和業務代碼
  • Control 控制: 自動化測試門禁

配套的技術工具包括 linter, 靜態檢查, 動態檢查和測試框架. 可關注的代碼品質指標也很多, 比如複雜度指標(圈複雜度, 認知複雜度, 嵌套層級深度, 類耦合度), 品質指標(重複率, 函數長度, 魔法數字), 以及測試指標(單元測試覆蓋率, 整合測試通過率, 用例有效性, 異常處理覆蓋率).

方法論和工具能幫助發現和解決問題, 代價是可能某程度上提升交付成本, 降低效率. 在有限資源下, 要選一種短中長期都能持續獲益的方法. 我的選擇是用單元測試作為開發過程中的輔助工具.

借助 AI 在不同角色語言之間的轉換, 開發可以融合 TDD, BDD, ATDD 的優勢, 快速生成用例表和用例代碼, 提升交付品質.

測試驅動: TDD/BDD/ATDD 與可測試代碼

三種測試驅動的差異如下:

維度TDDBDDATDD
驅動來源單元測試用戶行為場景驗收條件
參與者開發者業務+開發業務+測試+開發
測試層級單元測試整合/端到端測試驗收測試
工具示例JUnit, pytestCucumber, SpecFlowFitNesse, Robot Framework
典型輸出代碼覆蓋率報告可執行的用戶場景文檔驗收測試通過率
語言assert add(2,3) == 5自然語言場景 (Given-When-Then)業務語言 (驗收條件表格)

可測試代碼有四個核心原則: 單一職責(每個函數只做一件事), 依賴注入(透過參數傳遞依賴), 介面隔離(輸入輸出類型清晰), 狀態可觀測(提供查詢介面驗證內部狀態).

給 AI 的提示詞裡, 我建議直接寫 可擴展, 單一職責, 依賴注入. 我曾經嘗試讓 AI 控制生成的函數長度和檔案長度, 它的遵循程度很差; 但換成單一職責和依賴注入後, 生成的代碼明顯更符合預期. 過早優化是萬惡之源, 生成基礎數據結構和介面時, 提示 AI 可擴展性優先即可.

方便測試的代碼示例:

// user.h
typedef struct {
    int id;
    char name[50];
    int age;
} User;

// 記憶體儲存介面抽象
typedef struct {
    User* (*get_user)(int id);
    int (*save_user)(const User* user);
} UserStorage;

// 建立用戶 (依賴注入儲存介面)
User* create_user(const char* name, int age, UserStorage* storage) {
    if(strlen(name) >= 50 || age <= 0) return NULL;

    User* new_user = malloc(sizeof(User));
    new_user->id = generate_user_id();
    strncpy(new_user->name, name, 49);
    new_user->name[49] = '\0';
    new_user->age = age;

    if(storage->save_user(new_user) != 0) {
        free(new_user);
        return NULL;
    }
    return new_user;
}

不便測試的代碼示例:

// 緊耦合資料庫操作
void save_user_to_db(User* user) {
    MYSQL conn;
    mysql_init(&conn);
    if(!mysql_real_connect(&conn, "localhost", "root", "pass", "test", 0, NULL, 0)) {
        log_error("DB connection failed"); // 難以測試的日誌依賴
        exit(1); // 直接退出影響測試執行
    }
    // 拼接 SQL 並寫入資料庫
}

好的代碼潤物細無聲, 不好的代碼存在感強. 方便測試的函數回傳值明確, 錯誤處理規範, 傳入參數明確, 副作用隔離, 支援依賴注入.

AI 給出的代碼有時不是最佳實踐, 風格也有問題. 它雖然常常能解決問題, 但實際項目不是一錘子買賣, 必須考慮後續維護. 提示 AI 生成代碼時, 同樣要強調 可讀性優先, 可擴展, 單一職責, 依賴注入. 代碼風格問題不做更多討論, 可參考 Google 的代碼風格指南2.

實際開發的坑與權衡

需求在跨角色傳遞時, 會經歷多輪語言轉換, 每一輪都可能產生表述偏差. 例如客戶說的"集中查看"被後端理解成"單介面回傳"而沒有做分頁. 借助 AI 生成各角色語言模板, 可以減少人工轉換的信息損耗.

考慮不周是很多 bug 的根源. 借助 AI 設計用例場景, 它不僅能完成預期任務, 還能超預期地給出啟發, 補上發布指令者沒考慮到的細節. 在每一輪角色語言轉換中, 都可以和 AI 對話來完善漏洞. Markdown 是 AI 最容易理解的語言, 輸入輸出應盡量採用 Markdown 格式.

用例代碼有幾個必須注意的點: 用例隔離, 模擬依賴準確性, 斷言有效性, 邊界值, 異常處理, 性能, 並發. AI 會偷懶, 不會一次生成覆蓋所有場景的用例. 一次生成全部測試代碼對模型要求較高, 可以分多次生成.

以 Cursor 中的 Claude 模型 為例, 它對用例代碼的部分注意事項完成較好, 但常忘記用例隔離和異常場景. 加一條 user rule 可以稍微減少偷懶: 生成測試用例時注意用例文檔, 用例隔離. 不過它仍不會一次生成所有場景, 正常路徑, 異常路徑, 邊界值, 性能, 並發等場景需要多次對話補充.

一個可行的 AI 輔助測試工作流

結合設計文檔, 可以串起一條從場景到用例的流水線:

---
title: AI 輔助測試工作流
---
flowchart TB
    A[設計文檔] e1@--> B[生成驗收測試場景]
    B e2@--> C[生成數據結構和介面]
    C e3@--> D[生成端側用例表]
    D e4@--> E[生成用例代碼]
    D e5@--> F[生成業務代碼]
    E e6@--> G[本地門禁: 用例通過]
    F e7@--> G
    G e8@--> H{存在失敗用例}
    H e9@-->|是| E
    H e10@-->|否| I[提測]

    classDef input fill:#E3F2FD,stroke:#1565C0,color:#0D47A1;
    classDef gen fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20;
    classDef gate fill:#FFF3E0,stroke:#E65100,color:#E65100;
    classDef animate stroke:#EF6C00,stroke-width:2px,stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
    class A input;
    class B,C,D,E,F gen;
    class G,H,I gate;
    class e1,e2,e3,e4,e5,e6,e7,e8,e9,e10 animate;

各步驟的提示詞可以這樣組織: 根據產品設計文檔生成可驗收測試場景; 根據測試場景生成數據結構和介面; 根據測試場景和介面生成端側用例表; 根據介面和用例表生成用例代碼; 根據測試場景生成業務代碼並確保用例通過.

這裡有一個隱患: AI 既可能根據測試代碼改業務代碼, 也可能反过來改測試代碼. 如果兩邊都存在錯誤, AI 會迷惑, 隨著對話次數增加, 測試代碼和業務代碼會越來越亂, 此時必須人工干預.

一個貪大的提示詞是反例: 要求 AI 一次性覆蓋正常, 異常, 邊界值, 並發並生成代碼. 這類請求容易中斷. 正確做法是在 Cursor 裡拆分場景, 用多個請求分批生成. 性能測試用例需要大量數據, 應放到系統測試階段, 先讓 AI 寫隨機數據生成器, 再用生成的數據構造用例.

整合測試需要構建測試環境. 有的用例依賴外部工具, 比如微隔離依賴 iptables 和 ipset, 可以用 Docker 構建測試容器. 原則是: 人怎麼測, 就指揮 AI 實現相同的自動化步驟. 關於安全測試, 目前主要依賴經驗和分析, 還沒有成熟的 AI 輔助經驗.

總結

這條工作流相比不使用 AI 會產出更多文檔和測試代碼, 很難做到每個字都檢視. 但文檔比代碼更容易檢視, 尤其是用例表, 它對用例生成有準確的指導作用. 實際開發中, 我有時只檢視失敗的用例, 透過調試失敗用例來確認問題.

改進方向有三點: 設計階段子流程有交叉, 驗收測試場景可能依賴數據結構和介面; 端側用例表由研發自己關注; 提測前需要再次生成包含業務代碼細節的詳細測試場景.

最終收益不在測試覆蓋率數字, 而在信任: 借助單元測試這道本地門禁, 我對 AI 生成的代碼終於能做到心裡有底, 而不是把 vibing 出來的代碼不假思索地送進生產.


  1. Six Sigma 的定義與 DMAIC 方法參考維基百科詞條. ↩︎

  2. Google Style Guide 是 Google 公開的代碼風格指南. ↩︎