「單元測試要隔離外部依賴」是每個測試教學都會強調的第一課,AI 也把這句話學得很熟——熟到一個危險的程度:只要看到建構子裡有依賴注入,第一個反射動作就是把它 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:
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);
}
}
// 兩個純邏輯物件都用真實實例,測試同時驗證了三件事:
// 折扣邏輯本身、稅額邏輯本身、以及兩者串接起來的整體行為是否正確
第一個版本每次改動 PricingRule 或 TaxCalculator 的內部邏輯,這個測試都不會紅——因為它根本沒在測那兩段邏輯。第二個版本則會誠實反映任何一段邏輯壞掉的結果。
Mock 的價值在於隔離掉會讓測試變慢、不穩定、有副作用的東西,不是隔離掉一切——用 Mock 的數量取代真實協作越多,這個測試驗證到的「這套系統真的能動」的信心就越少。
回想你手上最近一個用 AI 生成的測試檔案:裡面 mock 掉的東西,有幾個是真正的外部依賴(資料庫、網路、檔案系統),又有幾個只是另一段純邏輯、被 mock 掉純粹是因為它出現在建構子參數裡?
明天用一個具體案例,示範一個被過度 mock 掉的測試怎麼通過了、卻完全沒抓到一個真正存在的整合 bug。