撰寫測試程式碼時,總會遇到一道牆。
「這是 Mock 還是 Stub?」讓人困惑的時刻。
明明都像「假的物件」,但程式碼審查時聽到「那不是 Mock,是 Stub」就會腦袋一片空白。
本文將搭配範例,清楚整理測試替身五兄弟(Dummy、Stub、Spy、Mock、Fake)的差異。
先直接說結論。
測試替身取決於「驗證什麼」。回傳值是 Stub,記錄呼叫是 Spy,判斷是否為預期呼叫是 Mock,像真的一樣運作則是 Fake。
記住這一句,就完成一半了。
測試替身到底是什麼?
測試替身(Test Double)一詞源自電影中的「特技替身(stunt double)」。
就像替身代替演員完成危險場面,它泛指代替真實物件的假物件。
這個概念由 Gerard Meszaros 在《xUnit Test Patterns》(2007)中系統化整理。
所以 Mock、Stub 都是測試替身的子類型。
為什麼要用?例如測試付款邏輯時,不可能真的呼叫信用卡公司的 API。
因為速度慢、會產生費用,網路中斷也會讓測試失敗。
因此要建立一個替身來取代真實物件。
Mock、Stub、Spy、Fake 差異一表整理
用文字說明容易混淆,先看表格。
| 種類 | 核心角色 | 驗證對象 |
|---|---|---|
| Dummy | 只填補位置 | 不驗證 |
| Stub | 回傳固定值 | 狀態(結果值) |
| Spy | 記錄呼叫紀錄 | 狀態+呼叫紀錄 |
| Mock | 判斷是否為預期呼叫 | 行為(呼叫) |
| Fake | 輕量且像真的一樣運作 | 狀態(結果值) |
這裡最重要的區分只有一條。
是驗證狀態,還是驗證行為。
Stub 和 Fake 看的是「結果是否如此」(狀態驗證)。
Mock 看的是「該方法是否真的被呼叫」(行為驗證)。
Spy 則在中間,悄悄記錄呼叫。
用程式碼看就很直觀
程式碼比文字更快理解。以使用者通知功能為例。在 Swift 中,通常會直接建立採用協定的測試替身。
先看 Stub:只回傳固定值的替身。
// getName固定為始終回傳「洪吉童」
final class UserRepositoryStub: UserRepository {
func getName(id: Int) -> String { "洪吉童" }
}
let service = GreetingService(repository: UserRepositoryStub())
// 測試只確認結果值(狀態)是否正確
#expect(service.greet(id: 1) == "洪吉童")
接著是 Mock,用來驗證「這個方法是否被呼叫」。
// 驗證通知是否真的已發送(被呼叫)
final class NotificationSenderMock: NotificationSender {
var sendCallCount = 0
func send(_ message: String) { sendCallCount += 1 }
}
let mockSender = NotificationSenderMock()
let service = UserService(sender: mockSender)
service.notifyUser(id: 1)
// 行為驗證: send是否剛好被呼叫 1次
#expect(mockSender.sendCallCount == 1)
看出 Stub 確認回傳值(結果),Mock 確認呼叫次數的差異了嗎?
這個觀點差異是理解測試替身的核心。
那 Spy 和 Fake 什麼時候用?
Spy會包住真實物件,保留原本行為,同時偷偷記錄呼叫紀錄。
當你想保留「真實邏輯」,又想知道呼叫了幾次時,就使用它。
在 Swift 中,常見做法是委派給真實實作,並將呼叫參數與次數記錄在屬性中的 Spy 類別。
Fake的行為類似真實物件,但實作輕量得多。
最常見的例子,是取代真實資料庫的記憶體內儲存庫,或以 Dictionary 建立的假儲存庫。
它像真的一樣運作,但不足以用於正式環境,就是測試專用實作。
總結如下。
- 只需要值 → Stub
- 需要驗證是否呼叫 → Mock
- 真實行為+呼叫紀錄都要 → Spy
- 需要像真的一樣運作的輕量實作 → Fake
常見問題
Q. Mock 和 Stub 在實務上可以混用嗎?
實務上,一個手寫測試替身類別常同時負責回傳值(Stub)與記錄呼叫(Mock),界線確實模糊。不過要區分目的是「提供值」還是「驗證呼叫」,測試意圖才會清楚。
Q. 聽說 Mock 用太多不好?
過度驗證行為,內部實作稍有變動測試就會大量失敗。因此建議盡量以狀態驗證(Stub・Fake)為主,只在必要的協作關係中使用 Mock。
不用死記五種類型,只要問自己:「現在要的是值,還是要驗證呼叫?」
只要回答這個問題,就能自然決定要建立哪種替身。
今天寫測試程式碼又搞混時,就再翻開這篇文章吧。祝大家的測試更加穩固!

