想知道 AI 寫的測試到底能不能失敗,就故意把它守護的程式碼改錯,再跑一次。如果測試仍然通過,說明它並沒有保護這段行為,整套測試再綠也沒用。測試通過只說明測試和當前程式碼彼此一致。
今天就能手動做:在測試聲稱守護的那一行引入一個小錯誤,只重跑這個測試,確認它因為行為斷言而失敗;然後恢復程式碼,確認它重新變綠。Stryker、PIT、mutmut 這類變異測試工具,就是把同一件事自動化,批量套用到許多小改動上。
要點
- 全綠不是證據。有用的測試,在它描述的行為被破壞時必須失敗。
- 最快的檢查:改壞被守護的那一行,只重跑這個測試,期待斷言失敗,再恢復。
- 崩潰、匯入錯誤、編譯錯誤,都不算測試抓到了缺陷。
- 把期望值改反,只能證明斷言被執行了,證明不了測試能發現錯誤程式碼。
- 變異測試把「改壞再看」自動化。存在等價變異體,100% 並不總能達到。
為什麼全綠的 AI 測試可能毫無價值?
因為在正確程式碼上通過的測試,在錯誤程式碼上也可能照樣通過。覆蓋率補不上這個缺口:PIT 文件指出,行覆蓋率只衡量哪些程式碼被執行,不衡量測試能否發現其中的錯誤。
生成的測試裡值得留意的模式:
- 只斷言形狀: 不為空、型別對、沒拋例外,但從不檢查值。
- 期望值抄自程式碼: 用實作本身的公式算期望值,或把當前程式碼的輸出直接貼進來。這樣測試會和缺陷保持一致。
- 把被測單元 mock 掉: 斷言檢查的是 mock,不是函式本身。
- 斷言呼叫而非結果: 「save() 被呼叫了」即使資料沒存對也能通過。
- 只測正常路徑: 一個典型輸入,沒有邊界或拒絕測試案例。
- 根本沒執行: 被跳過、過濾或沒被收集的測試不等於通過。要看報告的測試數,不只看結束碼。
不用專門工具,怎麼確認測試能失敗?
在測試對應的正式程式碼裡故意製造一個錯誤,並要求測試因此失敗。
- 從乾淨狀態開始。 先 commit 或 stash,確保能原樣恢復。記下測試名和它應守護的那一行。
- 只改壞一處:
>=改成>、-改成+、換一個常數,或提前返回錯誤值。保持程式碼能編譯。 - 只執行這一個測試,用測試執行器常規的單一測試選擇方式。
- 讀失敗資訊。 應該是關於行為的斷言失敗(值不對、狀態不對)。語法錯誤或匯入錯誤說明檢查根本沒發生,改得更小一點再試。
- 恢復後重跑。 確認 diff 已乾淨,同一個測試重新通過。
- 記一行: 測試名、改了什麼、失敗資訊、版本。
如果是缺陷修復,更乾淨的做法是把新測試放到修復前的提交上跑:那裡應失敗,在最終 diff 上應通過。如何驗證 AI 生成的缺陷修復 詳細介紹了這種先紅後綠的記錄方式。
把期望值改反(== 60 改成 == 61)是更弱的檢查。它能發現被跳過或沒被收集的測試,但你改的是測試而不是程式碼,所以無法說明錯誤的實作會不會被發現。
一個小教學練習
僅用於教學,不是 Boloop 的測試套件。弱測試只查形狀;強測試按規則檢查值(80 減去 25% 等於 60)。儲存為檔案後用 python3 執行。
def apply_discount(price, percent):
return round(price * (1 - percent / 100), 2)
def apply_discount_mutant(price, percent):
return round(price * (1 + percent / 100), 2) # deliberate bug
def test_weak(fn):
result = fn(80, 25)
assert result is not None
assert isinstance(result, float)
def test_strong(fn):
assert fn(80, 25) == 60.0
def check(test, fn):
try:
test(fn)
return "pass"
except AssertionError:
return "FAIL"
for name, fn in [("original", apply_discount), ("mutant", apply_discount_mutant)]:
print(f"{name}: weak={check(test_weak, fn)} strong={check(test_strong, fn)}")2026-11-08 用 Python 3.13 執行的輸出:
original: weak=pass strong=pass
mutant: weak=pass strong=FAIL兩個測試在正確程式碼上都是綠的,只有一個發現了缺陷。
變異測試什麼時候有用,什麼時候多餘?
當你希望「改壞再看」覆蓋許多行,而不只是你手挑的那一行時,它有用。這類工具會插入小錯誤(變異體),對每個變異體執行測試,報告有測試失敗(killed,被殺死)還是全部通過(survived,存活)。
- [Stryker](https://stryker-mutator.io/docs/): StrykerJS(JavaScript/TypeScript)、Stryker.NET(C#)、Stryker4s(Scala)。
- [PIT](https://pitest.org/): Java 與 JVM。它變異位元組碼;可從命令列、Ant 或 Maven 啟動,Gradle 由第三方元件整合。
- [mutmut](https://mutmut.readthedocs.io/en/latest/): Python。
mutmut run會呼叫 pytest,mutmut browse檢視變異體。需要支援 fork 的系統,所以在 Windows 上要在 WSL 裡執行。把變異體寫回磁碟前先提交程式碼。
按同一批文件解讀結果:
- Survived(存活) 指測試跑過了被變異的程式碼卻沒有失敗;No coverage(無覆蓋) 指根本沒有測試執行到它。
- Stryker 不把執行時錯誤和編譯錯誤的變異體計入變異分數。
- 等價變異體 行為與原本的程式碼相同,任何測試都殺不死。Stryker 表示它沒有確定的辦法找出它們。不要追求 100%。
- 成本: PIT 建議經常執行,但只針對改動過的程式碼;Stryker 的 FAQ 建議只變異必要的部分。
對一次性指令碼、還跑不穩定的測試套件,或者風險邏輯只有幾行、手動就能查完的情況,它屬於多餘。先修好不穩定和被跳過的測試。
適合誰?
適合要驗收 Claude Code、Codex、Copilot 或類似代理所寫測試的開發者和審查者,尤其是隨修復或新規則一起加進來的測試。
這不是完整的測試策略,替代不了真實使用者路徑檢查、安全審查或效能測試。能失敗的測試仍可能斷言了錯誤的規則;期望值必須來自約定的需求。
和 Boloop 其他指南的關係
它展開的是其中一步。AI 生成程式碼審查與驗收清單 把「證明測試能失敗」列為四項 AI 專屬檢查之一,本文講怎麼做。如何驗證 AI 生成的缺陷修復 把它用作先紅後綠。如果要讓代理設計測試案例或準備紅測試交接,test-case Skill 把這些模式分開處理。
常見問題
程式碼覆蓋率高就夠了嗎?
不夠。覆蓋率只說明哪些行被執行;沒有有效斷言的測試同樣能覆蓋它們。
能讓代理自己做失敗檢查嗎?
可以先讓它做,然後你在最終 diff 上自己重跑,並閱讀失敗資訊。代理的總結不是測試結果。
改壞程式碼後測試因匯入錯誤失敗,算嗎?
不算。它沒走到斷言。換一個更小、不影響程式碼載入的改動。
變異分數應該定多少?
本文讀過的文件沒有給出通用目標。把改動程式碼裡的存活變異體當作審查事項。
來源
- Stryker 文件:What is mutation testing?、Mutant states and metrics、Equivalent mutants、FAQ。
- PIT:首頁、Quickstart、Basic concepts、Mutators。
- mutmut 文件.
限制
Boloop 沒有為本文執行 Stryker、PIT 或 mutmut,也不報告任何變異分數。工具相關說法來自上述官方文件(2026-11-08 閱讀),不同版本行為可能變化。唯一執行過的程式碼是上面的教學練習,輸出如文中所示。