iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
Software Development

AI 時代的 TDD:讓 AI 寫 Code,但不要讓它決定品質系列 第 18 篇

Day 18:案例——100% 覆蓋率,但測試對重構完全沒有防護力

  • 分享至 

  • xImage
  •  

前言:覆蓋率報告顯示 100%,這不是應該最安心的狀態嗎?

「這個模組的覆蓋率報告顯示 100%,代表每一行、每一個分支都被測試跑過,重構起來應該很放心吧?」

昨天談到覆蓋率不是品質,只是一個統計數字。今天要用一個具體案例,把這句話講得更刺眼一點:不只是「覆蓋率高不等於品質好」,而是覆蓋率 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% 的測試套件,想知道它是不是真的有防護力,最快的驗證方式就是故意改壞一個分支的邏輯(像上面折扣寫反這樣),重新跑一次測試:

  • 如果測試變紅了,代表這份測試確實驗證了正確的業務規則。
  • 如果測試依然全綠,代表不管覆蓋率報告顯示多漂亮的數字,這份測試對「邏輯有沒有被改壞」這件事的防護力是零。

覆蓋率報告能告訴你的只有「這段程式碼有沒有被執行過」,它從來沒有能力回答「執行的結果對不對」——後面這個問題,永遠只有斷言的內容能回答。

今日思考題

回想你手上覆蓋率最高的那個模組:如果現在故意把裡面某一段邏輯的關鍵數字改掉,你有把握測試會變紅嗎?如果沒有把握,那份漂亮的覆蓋率數字,可能從一開始就沒有在保護你。

今日重點回顧

  • 覆蓋率 100% 只代表程式碼被「執行到」,不代表測試「驗證到」任何有意義的東西
  • 型別檢查、非空檢查這類弱斷言,可以讓覆蓋率衝滿,卻攔不下業務邏輯被改壞
  • 檢驗方法很簡單:故意改壞邏輯,看測試會不會變紅——變紅才代表真的有防護力
  • 覆蓋率工具回答的是「有沒有執行」,斷言內容才回答「執行的結果對不對」,兩者是完全獨立的兩個問題

明日預告

明天要處理另一種容易被忽略的細節:讓 AI 寫 Test Double 時,Dummy、Stub、Fake、Spy、Mock 這五種類型該怎麼選——選錯類型,也會製造出跟今天案例類似的假安全感。


上一篇
Day 17:Coverage 不是品質——AI 很容易把「覆蓋率高」當成「品質好」的證據
下一篇
Day 19:讓 AI 寫 Test Double 時要注意的邊界——Dummy/Stub/Fake/Spy/Mock
系列文
AI 時代的 TDD:讓 AI 寫 Code,但不要讓它決定品質 共 21 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言