「這個模組的覆蓋率報告顯示 100%,代表每一行、每一個分支都被測試跑過,重構起來應該很放心吧?」
昨天談到覆蓋率不是品質,只是一個統計數字。今天要用一個具體案例,把這句話講得更刺眼一點:不只是「覆蓋率高不等於品質好」,而是覆蓋率 100% 的測試,實際上可以對重構提供零防護力——不是防護力不夠,是完全沒有。
假設有一段判斷會員折扣的邏輯:
❌ 待測程式碼
function calculateDiscount(memberLevel: string, amount: number): number {
if (memberLevel === "gold") {
return amount * 0.8;
}
if (memberLevel === "silver") {
return amount * 0.9;
}
return amount;
}
搭配的測試長這樣:
❌ 覆蓋率 100%,但沒驗證任何業務規則
describe("calculateDiscount", () => {
it("gold 會員應該回傳一個數字", () => {
const result = calculateDiscount("gold", 1000);
expect(typeof result).toBe("number");
});
it("silver 會員應該回傳一個數字", () => {
const result = calculateDiscount("silver", 1000);
expect(typeof result).toBe("number");
});
it("一般會員應該回傳一個數字", () => {
const result = calculateDiscount("normal", 1000);
expect(typeof result).toBe("number");
});
});
三個分支都被執行到,覆蓋率報告會顯示這支函式 100%。但這三個測試斷言的內容,只驗證了「回傳值的型別是數字」,完全沒有驗證折扣算對了沒有。
現在假設有人(或 AI)重構這段邏輯時,不小心把 gold 跟 silver 的折扣寫反了:
✅ 重構後(邏輯已經壞掉,但測試看不出來)
function calculateDiscount(memberLevel: string, amount: number): number {
if (memberLevel === "gold") {
return amount * 0.9; // 原本應該是 0.8,寫錯了
}
if (memberLevel === "silver") {
return amount * 0.8; // 原本應該是 0.9,寫錯了
}
return amount;
}
三個分支還是全部被執行到,覆蓋率報告依然是 100%,測試也依然全部通過——因為不管折扣算出來是多少,只要回傳的是一個數字,斷言 typeof result === "number" 就會成立。這份測試套件從頭到尾都沒有能力偵測「gold 會員的折扣被寫錯」這件事,覆蓋率報告卻一路顯示綠燈,給了一個完全假的安全感。
覆蓋率工具量測的是「這一行程式碼有沒有被執行」,這是一個純粹的執行軌跡問題,跟斷言內容完全無關。一段程式碼可以被執行一萬次,只要每次跑完都沒人去檢查「執行的結果對不對」,覆蓋率照樣可以衝到 100%。
真正決定測試有沒有防護力的,是斷言驗證了什麼,不是程式碼被執行了幾次。 這正是這個系列從 Day 04 開始反覆講的假陽性模式——這裡的三個測試都屬於「斷言太弱」這一類:型別檢查、非空檢查、不拋例外檢查,這些斷言在語法上完全正確,也確實會讓測試變綠,卻沒有觸及函式真正該負責的那件事:算出正確的折扣金額。
用一組對照來看差異:
❌ 只驗證型別,沒驗證業務規則:
expect(typeof result).toBe("number");
→ 不管折扣算對算錯,只要回傳的是數字,這個斷言永遠成立
✅ 驗證具體的業務規則:
expect(calculateDiscount("gold", 1000)).toBe(800);
expect(calculateDiscount("silver", 1000)).toBe(900);
expect(calculateDiscount("normal", 1000)).toBe(1000);
→ 折扣算錯的瞬間,斷言就會失敗,測試才真正有防護力
修正後的測試,覆蓋率報告上看到的數字不會有任何變化——一樣是三個分支、一樣是 100%。唯一改變的是斷言的內容,但這個改變決定了測試從「裝飾品」變成「安全網」。
Day 07 講過一個判斷測試有效性的通用方法:故意讓實作退回錯誤版本,看測試會不會抓到。同樣的方法可以直接套用在這裡——如果你手上有一份覆蓋率 100% 的測試套件,想知道它是不是真的有防護力,最快的驗證方式就是故意改壞一個分支的邏輯(像上面折扣寫反這樣),重新跑一次測試:
覆蓋率報告能告訴你的只有「這段程式碼有沒有被執行過」,它從來沒有能力回答「執行的結果對不對」——後面這個問題,永遠只有斷言的內容能回答。
回想你手上覆蓋率最高的那個模組:如果現在故意把裡面某一段邏輯的關鍵數字改掉,你有把握測試會變紅嗎?如果沒有把握,那份漂亮的覆蓋率數字,可能從一開始就沒有在保護你。
明天要處理另一種容易被忽略的細節:讓 AI 寫 Test Double 時,Dummy、Stub、Fake、Spy、Mock 這五種類型該怎麼選——選錯類型,也會製造出跟今天案例類似的假安全感。