別相信 Agent 寫的測試
原文由 Alex O'Callaghan 于 發布,訂閱此部落格
我用 Agent 將 53 個 Enzyme 測試套件遷移到 React Testing Library。測試都通過了,程式碼看起來也很合理,但仔細一看才發現,有些測試根本抓不到回歸問題,有的斷言了錯誤的對象,還有些根本什麼都沒測到。
如果是 Agent 產生的測試,這種問題特別容易被忽略,因為輸出的成果看起來很完整,程式碼乾淨,而且數量龐大。
從 Enzyme 遷移到 RTL
那些 Enzyme 測試早就成了技術債,在待辦清單裡被列為低優先順序好幾年。這正好是那種定義明確、機械化的工作,理應是 Agent 最擅長的。我依照複雜度分批處理,用我的 prepare-mr-skill 來整理 commit、撰寫說明,結果看起來很成功:測試都遷移完了,也都通過了,程式碼的變更看起來也算合理。
在合併之前,我們照例讓團隊進行審查,才發現有些測試看似正確,卻完全抓不到回歸問題。舉例來說,有個測試斷言某個具有特定 ARIA role 的元素不該被渲染,但那個 role 本來就是無效的,元件根本不可能渲染出那樣的元素。還有對 disabled 和 aria-disabled 的混淆,這兩個屬性行為不同、對無障礙也很重要,但看起來很像,以至於一個看似合理的測試就算寫錯了也不會失敗。
為什麼會這樣
這種問題並不是 Agent 才有,手寫測試也很容易寫成這樣。不過,人寫出不好的斷言時,通常對元件有一定了解,可能會覺得這個測試過得太輕鬆而起疑。Agent 則沒有這種直覺,它只是從實作中做模式比對,產出一個看似正確的東西就繼續往下做。而且,Agent 能產出的程式碼量非常大,讓這類問題更容易溜過審查。
當實作已經存在時(例如遷移測試、補測試覆蓋率、驗證多年前寫的測試),測試驅動開發(TDD)那種自然的防護機制就派不上用場了。你是在已經能運作的程式碼上寫測試,所以錯誤的斷言根本沒機會失敗。唯一能抓到問題的方法,就是故意把元件改壞:暫時修改元件,確認測試會因此失敗,再改回來。
讓驗證真正落實
我一開始的直覺是修改 prompt,強迫 Agent 檢查這類錯誤。我更新了 RTL 遷移用的 skill,明確要求每個測試都必須透過失敗情境來驗證。
但效果並不穩定。Agent 會考慮這個要求,有時會抓到一些問題,然後就繼續往下做。它把驗證當成一個待辦清單上的勾選項目,而不是必須遵守的限制。
部分原因出在結構上。當你要求 Agent 一次遷移大量測試時,任務很長,上下文很快就被塞滿。隨著上下文變長,模型會開始失去焦點,不再緊盯你的指令。有些模型還會出現「上下文焦慮」,在接近上下文視窗的尾端時就開始提早收尾。這會導致 Agent 開始遺忘或跳過這類驗證指令。
我試著改成一次只遷移一個測試套件,而不是一次全部跑完,心想較短的上下文視窗能減輕完成任務的壓力,讓 Agent 有更多餘裕慢慢執行驗證步驟。Agent 確實比較願意檢查了,但常常只決定驗證「幾個關鍵測試」,而不是每一個都驗證。
最後,我嘗試把遷移和審查拆成兩個獨立的 Agent 任務。我寫了一個 bash 腳本,逐一遍歷每個測試套件,並依序執行兩個 Agent prompt:一個負責遷移,一個負責審查。
#!/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 廠商的帳單越來越高。
結論
那個雙 prompt 的腳本,並不是更好的 prompt,而是不同的架構。當你要求同一個 Agent 在同一輪同時完成遷移和驗證,任務結構本身就不利於驗證。當某個流程步驟總是被跳過時,解法通常不是寫更好的指令,而是設計一個讓那個步驟無法被跳過的工作流程。
從概念上來說,這裡審查者在做的其實就是突變測試(mutation testing):針對每個斷言,去問如果元件被改動,測試是否會跟著失敗。在測試流程中導入像 Stryker 這樣的突變測試工具,有助於抓出這類問題,也能避免讓 Agent 手動執行而降低成本。
花時間設計 Agent 的工作流程本身就是工作的一部分,在評估使用 Agent 帶來的生產力提升時,應該把這點也算進去。這正是產出值得信任,還是僅僅看起來值得信任的差別。
隨機一篇部落格
留言
登入後參與討論