iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0
Software Development

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

Day 13:Mock 的濫用——AI 傾向 mock 一切,隔離過頭反而測不到整合行為

  • 分享至 

  • xImage
  •  

前言:「單元測試要隔離依賴」,這句話本身沒有錯

「單元測試要隔離外部依賴」是每個測試教學都會強調的第一課,AI 也把這句話學得很熟——熟到一個危險的程度:只要看到建構子裡有依賴注入,第一個反射動作就是把它 mock 掉。

問題是,「隔離依賴」講的是隔離那些讓測試變慢、變不穩定、有副作用的外部依賴(資料庫、網路、檔案系統),不是「隔離所有跟這個類別打交道的東西」。AI 分不清楚這個界線,於是把待測物件真正該協作的鄰居也一併 mock 掉,測試看起來乾淨、跑得飛快、全部綠燈,卻沒有驗證到任何一段真實的整合行為。

今日目標

  • 理解「隔離外部依賴」跟「隔離所有依賴」是兩件不同的事
  • 看懂 AI 過度 mock 時的典型訊號:mock 的物件比真實邏輯還多
  • 學會判斷「這個依賴該不該被 mock」的具體標準
  • 掌握一組 ❌/✅ 對照,分辨過度隔離跟合理隔離的測試

為什麼 AI 特別容易 mock 過頭

AI 產生測試的思路通常是「先看建構子要什麼、就把每一個都準備一份假的」。這個思路對「連資料庫」「打 API」這種真正的外部依賴是對的,但套用到「這個類別跟另一個純邏輯物件協作」時就會出錯——AI 沒有能力判斷「這個依賴是不是外部世界的邊界」,它只看得到「這是一個建構子參數」,於是一律套用同一招。

結果是:兩個原本該真的互相呼叫、互相驗證介面契約的物件,被測試硬生生拆開。各自的測試都通過了,但沒有任何一個測試驗證過「這兩個物件真的接得起來」——直到整合測試、甚至上線後才發現一方回傳的資料結構,跟另一方預期的完全對不上。

業界常見的 Dummy、Stub、Fake、Spy、Mock 五種測試替身分類(源自 Gerard Meszaros《xUnit Test Patterns》,Martin Fowler 在《Mocks Aren't Stubs》裡引用並闡述了 state verification 跟 behavior verification 的差異),各自的用途不同:Stub 是為了讓測試能跑(提供固定的回傳值),Mock 是為了驗證互動行為(斷言某個方法有沒有被呼叫)。這個分類本身沒有「該不該用」的立場,但AI 常常不管三七二十一先套用 Mock,即使這裡真正需要的可能只是一個 Stub,或者根本不需要測試替身,讓真實物件協作反而更能驗證行為

判斷該不該 mock 的具體標準

一個依賴值不值得 mock,可以問自己這幾個問題:

  • 這個依賴會不會讓測試變慢或不穩定?(資料庫連線、網路呼叫、檔案 I/O)——會,才考慮 mock。
  • 這個依賴有沒有副作用是這次測試不想觸發的?(寄信、扣款、寫入外部系統)——有,才考慮 mock。
  • 這個依賴只是另一段純邏輯,跟待測物件在同一個 process 裡協作嗎?——如果是,讓它們真的互動,測試才驗證得到介面契約有沒有對上。

用一組對照來看這個差異:

❌ 全部 mock:
class OrderTotalCalculatorTest {
    @Test
    void calculatesTotalWithDiscount() {
        var pricingRule = mock(PricingRule.class);
        var taxCalculator = mock(TaxCalculator.class);
        when(pricingRule.applyDiscount(100)).thenReturn(90);
        when(taxCalculator.calculate(90)).thenReturn(9);

        var calculator = new OrderTotalCalculator(pricingRule, taxCalculator);
        var total = calculator.calculateTotal(100);

        assertEquals(99, total);
    }
}
// pricingRule 跟 taxCalculator 都是純邏輯物件,沒有外部依賴、沒有副作用,
// 卻被 mock 成回傳固定值——這個測試只驗證了
// OrderTotalCalculator 有沒有正確呼叫兩個假物件,
// 完全沒驗證真實的折扣計算跟稅額計算邏輯接不接得上

✅ 只 mock 真正的外部依賴:
class OrderTotalCalculatorTest {
    @Test
    void calculatesTotalWithDiscount() {
        var pricingRule = new PercentageDiscountRule(0.1);
        var taxCalculator = new FlatRateTaxCalculator(0.1);

        var calculator = new OrderTotalCalculator(pricingRule, taxCalculator);
        var total = calculator.calculateTotal(100);

        assertEquals(99, total);
    }
}
// 兩個純邏輯物件都用真實實例,測試同時驗證了三件事:
// 折扣邏輯本身、稅額邏輯本身、以及兩者串接起來的整體行為是否正確

第一個版本每次改動 PricingRuleTaxCalculator 的內部邏輯,這個測試都不會紅——因為它根本沒在測那兩段邏輯。第二個版本則會誠實反映任何一段邏輯壞掉的結果。

Mock 的價值在於隔離掉會讓測試變慢、不穩定、有副作用的東西,不是隔離掉一切——用 Mock 的數量取代真實協作越多,這個測試驗證到的「這套系統真的能動」的信心就越少。

今日思考題

回想你手上最近一個用 AI 生成的測試檔案:裡面 mock 掉的東西,有幾個是真正的外部依賴(資料庫、網路、檔案系統),又有幾個只是另一段純邏輯、被 mock 掉純粹是因為它出現在建構子參數裡?

今日重點回顧

  • 「隔離外部依賴」不等於「隔離所有依賴」,AI 常常分不清楚這個界線
  • 過度 mock 的訊號:測試裡 mock 的物件,比真實跑的邏輯還多
  • 判斷該不該 mock 的標準:會不會讓測試變慢/不穩定、有沒有不想觸發的副作用、是不是同 process 裡的純邏輯協作
  • Mock 的數量越多,測試驗證到「系統真的接得起來」的信心就越少

明日預告

明天用一個具體案例,示範一個被過度 mock 掉的測試怎麼通過了、卻完全沒抓到一個真正存在的整合 bug。


上一篇
Day 12:測試命名——AI 寫的測試名稱看起來詳細,實際傳達了什麼?
系列文
AI 時代的 TDD:讓 AI 寫 Code,但不要讓它決定品質13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言