別輕信 Agent 寫的測試
我用 Agent 將 53 個 Enzyme 測試套件遷移到 React Testing Library。測試全數通過,程式碼看起來也很合理,但仔細檢視後,我們發現有些測試根本抓不到回歸問題,有些斷言的對象錯誤,還有些幾乎沒有測試到任何東西。
對於 Agent 產生的測試,這類問題很容易被忽略,因為產出的成果看起來很精緻、程式碼很乾淨,而且數量龐大。
從 Enzyme 遷移到 RTL
這些 Enzyme 測試是積欠多年的技術債,在待辦清單上一直被列為低優先順序。這正是 Agent 應該擅長的那種定義明確、機械化重複的工作。我依照複雜度分批處理,使用我的 prepare-mr-skill 來組織 commit 並撰寫說明,結果看起來是成功了。至少表面上看起來是如此:測試已遷移、全部通過,程式碼的變更看起來也合理。
在合併之前,我們按照慣例讓團隊審查這些 MR,結果發現有些測試看似正確,卻無法捕捉到回歸問題。舉例來說,有個測試斷言某個具有特定 ARIA role 的元素未被渲染,但該 role 本身就是無效的,這個元件從一開始就不可能渲染出該元素。還有混淆 disabled 和 aria-disabled 的情況,這兩個屬性行為不同、對無障礙至關重要,但外觀相似到足以讓一個看似合理的測試寫錯卻仍能通過。
為什麼會發生這種情況
這個問題並非 Agent 獨有,手寫測試也很容易寫成這樣。不過,人類在寫出糟糕的斷言時,通常對元件有一定了解,可能會察覺這個測試似乎過於簡單。Agent 則沒有直覺,它只是從實作中進行模式比對,產出看似正確的內容後就繼續往下執行。此外,Agent 能產生的程式碼量龐大,也讓這類問題更容易被忽略。
當實作已經存在時(例如遷移測試、補齊覆蓋率、驗證多年前撰寫的測試),Test Driven Development(測試驅動開發)(TDD)自然形成的防護機制就派不上用場。你是在針對已可運作的程式碼撰寫測試,因此錯誤的斷言根本沒有機會失敗。唯一能發現問題的方法,就是刻意破壞元件:暫時修改元件,確認測試能否捕捉到問題後,再還原變更。
讓驗證真正落實
我的第一個直覺是修正提示詞,強制要求 Agent 檢查這類錯誤。我更新了我的 RTL 遷移 skill,明確要求每個測試都必須透過失敗狀態進行驗證。
但這並沒有穩定地發揮作用。Agent 會考慮這項要求,有時會捕捉到一些問題,然後就繼續往下做。它把驗證當成待辦清單上可勾選的項目,而非必須遵守的限制條件。
問題有一部分是結構性的。當你要求 Agent 在單次執行中遷移大量測試時,任務時間很長,上下文也隨之被填滿。隨著上下文增長,模型會開始失去對指令的專注。有些模型還會出現「context anxiety(上下文焦慮)」的現象,也就是在接近上下文視窗尾聲時,會提早開始收尾。這會導致 Agent 開始遺忘或跳過這類驗證指示。
我嘗試改為逐個測試套件執行遷移,而非一次完成,心想較短的上下文視窗能減輕完成壓力,讓 Agent 有更多餘裕放慢腳步執行驗證步驟。Agent 檢查的機率確實變高了,但往往會自行決定只驗證「幾個關鍵測試」,而非每一個測試。
最後,我決定將遷移和審查步驟拆成兩個獨立的 Agent 任務。我寫了一個 bash 腳本,逐一遍歷每個測試套件,並依序執行兩個 Agent 提示詞:一個負責遷移,一個負責審查。
#!/bin/bash
echo "Starting migration..."
declare -a files=(
"path/to/test-suite.test.tsx"
)
for i in "${files[@]}"; do
echo "Migrating $i"
agent -p --force --output-format stream-json --stream-partial-output --model claude-4.6-sonnet-medium \
"Migrate $i to use RTL. Ensure linting passes and then commit your changes in a single commit."
agent -p --force --output-format stream-json --stream-partial-output --model claude-4.6-sonnet-medium \
"Review the RTL tests in $i -
* Identify any unnecessary tests, remove them
* Identify any tests not following best practices, fix them
* For every single test intentionally change the implementation to break the specific feature tested and confirm the test fails as expected, fix them if they don't
Ensure linting passes and then commit your changes in a single commit."
echo "Migrated $i"
done在驗證任務更為聚焦的情況下,Agent 自行建立了待辦清單,並徹底逐一檢查每個測試,不僅捕捉到問題,還做出了一些超出審查所發現範圍的改進。
成本是多少
這個方法奏效了,但成本也顯著增加。所有遷移都是透過 Cursor 使用 claude-4.6-sonnet-medium 針對 22 個測試套件批次執行:
| 作法 | 成本 |
|---|---|
| 單次批次處理 | ~$13 |
| 逐套件處理 | ~$32 |
| 逐套件處理+審查 | ~$62 |
逐套件的審查流程解決了先前人工審查發現的所有問題,但相較於單次批次處理,成本增加了將近 5 倍。
Agent 讓我們能做更多事,完成那些原本只會一直躺在待辦清單上的現代化工作。但要把事情做好、產出值得信賴的成果,成本比我們預期的要高。我認為既然要做,就應該做好,但在所需時間大幅縮短的同時,這項工作實際的金錢成本之間仍存在取捨。
組織必須有意識地思考,如何讓提升的生產力真正轉化為營收成長,而不只是讓 AI 廠商的帳單節節攀升。
啟示
這個雙提示詞腳本並不是更好的提示詞,而是不同的架構。當你要求同一個 Agent 在同一次執行中同時完成遷移和驗證時,任務結構本身就不利於驗證。當某個流程步驟總是被遺漏時,解法通常不是寫出更好的指示,而是設計一個讓該步驟無法被跳過的工作流程。
從概念上來說,審查者在這裡做的就是 mutation testing(突變測試):針對每個斷言,檢視對元件的變更是否會導致測試失敗。將像 Stryker 這類 mutation testing 工具整合到測試流程中,有助於捕捉這些問題,並避免由 Agent 手動執行而降低成本。
投入時間設計 Agent 工作流程本身就是工作的一部分,在評估使用 Agent 所帶來的生產力提升時,也應將其納入考量。這正是產出值得信賴與產出看似值得信賴之間的差別。
隨機一篇部落格