測試替身:Stub vs. Mock (Test Doubles)

★★★★★Draft

FromThe Art of Unit Testing

📚 From the Books

測試替身:Stub vs. Mock (Test Doubles)Testing

當待測程式碼依賴檔案系統、網路、時間這些你控制不了的東西,測試就會因為外部原因而失敗——即使邏輯是對的。測試替身(test double)就是那個受控的替代品:像 NASA 用模擬器訓練太空人,你用假的協作物件,把待測程式碼放進一個可控的環境裡。關鍵是分清兩種替身——stub 餵輸入、mock 驗互動

🧠 Why It Matters

NOTE

灌溉系統的比喻:狀態測試是讓系統跑 12 小時、再去看土壤濕不濕——真實但昂貴;互動測試是在水管末端裝記錄器,只驗「有沒有在對的時間、放對的次數水」——不必真的種一棵樹。

⚖️ Stub vs. Mock

Stub — 餵假輸入,斷言打在待測類別上
// 待測:LogAnalyzer 依賴檔案系統判斷副檔名是否合法
// Stub 假裝成設定來源,回傳可控的值;我們斷言的是 analyzer 的「回傳值」
test("valid extension is accepted", () => {
  const configStub = { isSupported: () => true }; // stub:只提供輸入
  const analyzer = new LogAnalyzer(configStub);

  const result = analyzer.isValidLogFileName("data.log");

  expect(result).toBe(true); // ← 斷言打在待測類別,stub 不決定成敗
});

Stub 只是輔助測試順利運行,本身不會讓測試 fail。

Mock — 驗證互動,斷言打在替身上
// 待測:當檔名不合法時,analyzer「必須呼叫 logger 記錄一次」
// 「有沒有呼叫 logger」本身就是最終結果 → 用 mock 驗互動
test("an invalid filename is logged", () => {
  const loggerMock = { calls: [], log(msg) { this.calls.push(msg); } };
  const analyzer = new LogAnalyzer({ isSupported: () => false }, loggerMock);

  analyzer.isValidLogFileName("data.xxx");

  expect(loggerMock.calls.length).toBe(1); // ← 斷言打在 mock 身上
});

待測類別與 mock 之間的溝通被記錄下來,測試透過 mock 驗證互動是否正確。

WARNING

一個測試裡通常只該有一個 mock——因為 mock 代表「這次要驗證的那個互動」。其餘協作物件都用 stub。滿場都是 mock,等於把實作細節焊死在測試裡,稍一重構就整片變紅。

🔑 Takeaways

✍️ My Notes

No notes yet — jot your takeaways or Q&A here.

📖 Further Reading