「這個模組的測試覆蓋率有 95%,理論上應該很安全,為什麼團隊還是不敢動它?」
上一篇(Day 16)談到「誰來判斷一個測試值不值得留」,收尾了第二部。今天正式進入第三部——讓 AI 遵守 TDD 循環的具體做法,第一站就要拆穿一個最容易被誤信的數字:覆蓋率。
覆蓋率高卻沒人敢重構,這個矛盾在 AI 協作的專案裡特別常見,原因很簡單:當「把覆蓋率衝高」變成一個明確、可以下達的指令時,AI 會非常擅長完成它——擅長到讓你誤以為目標達成了,而實際上你要的東西根本不是覆蓋率本身。
覆蓋率工具(不管是哪個語言生態的實作)做的事情本質上很單純:追蹤程式執行的過程中,哪些行、哪些分支被跑到過。它回答的問題是「這段程式碼有沒有被執行」,它完全不知道、也沒辦法知道測試執行完之後有沒有斷言任何東西、斷言得對不對。
這代表一個測試函式裡只要呼叫了目標程式碼、什麼斷言都不寫,覆蓋率工具一樣會把對應的行標記成「已覆蓋」。這不是覆蓋率工具的缺陷,是它的設計範圍——它從一開始就不是拿來衡量「驗證品質」的工具,只是常常被誤用成這個角色。
當覆蓋率變成一個明確可以量化、可以被要求「達到 90% 以上」的目標時,AI 很容易找到「數字最快變好看」的路徑,而不是「程式碼最被確實驗證」的路徑:
null」「有這個屬性」,而不是斷言「值等於什麼」if/else 兩條分支都被覆蓋到,隨便塞一個能走到那個分支的輸入,不管這個輸入在真實情境下合不合理用一組對照來看這個差異:
❌ 為了覆蓋率而寫的測試:
test('processOrder covers the discount branch', () => {
const order = { total: 100, isVip: true };
const result = processOrder(order);
expect(result).toBeDefined();
});
→ processOrder 內部的折扣邏輯確實被執行到了(分支覆蓋率因此上升),
但這個測試完全沒驗證折扣算得對不對,
就算折扣邏輯本身寫錯,這個測試也會一路綠燈
✅ 驗證正確性而不是只求覆蓋:
test('processOrder_VIP會員訂單_應套用九折並回傳正確金額', () => {
const order = { total: 100, isVip: true };
const result = processOrder(order);
expect(result.finalAmount).toBe(90);
expect(result.discountApplied).toBe(true);
});
→ 同樣執行到折扣分支,但這次斷言了具體的期望值,
折扣邏輯算錯的話,這個測試會真的變紅
兩段程式碼在覆蓋率報告上看起來一模一樣——同一行、同一個分支都被標記成「已覆蓋」。覆蓋率工具沒有能力分辨這兩者的差別,只有人去讀測試內容才分辨得出來。
判斷一份覆蓋率報告可不可信,不需要複雜的工具,只需要問一個問題:如果我故意把這一行的邏輯改錯,會不會有測試因此變紅?
這正是這個系列在 Day 07 提過的「先看紅燈」精神的延伸——只是這次不是問「這個測試有沒有紅過」,而是問「這一行被覆蓋到的那個測試,紅得起來嗎」。如果答案是「不會,因為那個測試根本沒斷言這一行的邏輯結果」,那這行的覆蓋率是虛的,儘管報告上顯示著漂亮的綠色。
覆蓋率報告只能告訴你「哪裡完全沒有安全網」(未覆蓋的行),它沒辦法告訴你「哪裡的安全網其實是假的」(覆蓋了但沒驗證的行)——後者往往比前者更危險,因為它給了你一種錯誤的安全感。
覆蓋率不是沒用,只是它的正確用法是診斷工具而不是品質指標:它適合拿來回答「這段程式碼完全沒有任何測試碰過嗎」這種存在性問題,也適合拿來找出「這個複雜的條件邏輯裡,是不是有一條分支從來沒被走過」這種盲點。但它沒辦法回答「這些測試寫得好不好」,這個問題只能靠讀測試內容本身。
當你要求 AI 提升覆蓋率時,更精確的做法是同時要求它說明「新增的每個測試驗證了什麼具體的業務結果」,而不是只回報「覆蓋率從 78% 提升到 92%」這個數字本身。
回想你手上覆蓋率最高的那個模組:如果你隨機挑一個被標記為「已覆蓋」的分支,故意改錯它的邏輯,你有把握測試會變紅嗎?如果沒把握,那個覆蓋率數字可能比你以為的更空洞。
明天用一個具體案例,把今天講的東西完全落地:一個 100% 覆蓋率的模組,重構的時候測試卻完全沒有攔住任何一個真正的錯誤——覆蓋率跟保護力之間的落差,實際發生時長什麼樣子。