來源於《有效的單元測試》系列文章3.2 測試替身的類型:https://yq.aliyun.com/articles/118921
3.2 測試替身的類型
你見過了使用測試替身的各種原因,我們也暗示了有多種測試替身可供選擇。我們來仔細看看那些類型吧。圖3.3展示了這把大傘下的四種對象。
既然我們已經制定了測試替身的分類,現在就來認識一下它們,並了解相互的區別,以及運用它們的典型目的。我們先從最簡單的開始。
3.2.1 測試樁通常是短小的
我這樣來定義它:樁(名詞),截斷的或非常短的物體。
這衍生出測試樁的精確定義。測試樁(簡稱樁或Stub)的目的是用最簡單的可能實現來代替真實實現。最基本的實現例子就是一個對象的所有方法都只有一行,且它們各自返回一個適當的默認值。
假如你負責的代碼應當對自己的操作生成一段審計日志,並通過叫做Logger的接口寫入遠程日志服務器。假如Logger接口僅僅定義了一個方法來產生此類日志,那么Logger接口的樁看起來是這樣:
有沒有注意到log()方法其實什么都沒做?這是樁的典型例子——什么都不做。畢竟,你正是對真實Logger實現打樁,因為你在測試時完全不在乎日志,那么又何必真寫日志呢?但是有時候什么都不做也不行。例如,如果Logger接口還定義了一個方法來確定當前設置的日志級別(Log Level),那么樁實現看起來可能是這樣:
我們在這個類中硬編碼了getLogLevel()方法,它總是返回LogLevel.WARN。有沒有搞錯?大部分情況下這絕對沒問題。畢竟,我們有三個充分的理由來使用測試樁代替真實Logger實現:
1.?我們的測試不關心被測代碼所寫的日志。
2.?我們沒有運行日志服務器,所以測試會悲劇地失敗。
3.?我們也不希望測試套件在控制台中輸出大量字節(更別提將所有數據寫入文件了)。
簡而言之,Logger樁實現完美地滿足了我們的需要。
有時候,簡單的硬編碼返回語句和一堆空的void方法還不夠。有時候你至少需要填充一些行為,而有時候你需要測試替身根據收到的消息種類來表現出不同的行為。這些情況下,你會借助偽造對象。
3.2.2 偽造對象做事不產生副作用
比起Stub,偽造對象(簡稱Fake)是一種更加復雜的測試替身。Stub可以返回硬編碼值,而每個測試可能需要有差異地實例化來返回不同值,以模擬不同的場景。Fake更像是真實事物的簡單版本,優化地偽造真實事物的行為,但是沒有副作用或使用真實事物的其他后果。
持久化對象是采用Fake的典型例子。假設應用程序架構是這樣的:一些存儲對象提供持久化服務,它們知道如何存儲和查找指定的對象類型。這種存儲對象可能提供的API如下:
對於使用存儲對象的應用程序,如果沒有這種測試替身,測試全都將試圖訪問真實的數據庫。要是對UserRepository接口打樁,令其精確地返回測試所需,你就會感覺好一些。但是模擬更復雜的場景肯定會越發復雜。另一方面,由於UserRepository接口足夠簡單,以至於你可以實現一個愚蠢而簡單的內存數據庫,它只提供基本的數據類型。代碼清單3.4提供了一個例子。
用這種另類實現來替換真實事物的優點在於,它像只鴨子那樣嘎嘎叫,還能搖擺,但它搖擺得比真鴨子要快——即使每次查找一個User時都循環一個包含50個條目的列表。
測試樁和偽造對象往往是救命稻草,你可以在測試時用它們替換掉緩慢的真實事物,以及鞭長莫及的依賴。然而,這兩種基本的測試替身不總是夠用。有時你發現自己面對一堵牆,希望自己能像千里眼一樣看透它——為了驗證代碼行為符合預期。那些情況下,你可能會求助於測試間諜。
3.2.3 測試間諜偷取秘密
你如何測試下列方法?
大多數人會說,把這個那個傳進去,然后檢查返回值是什么的。那可能沒問題。畢竟正確的返回值是你最關心的。那么,下列方法又如何測試?
這里並沒有返回值可以用來斷言。這個方法所做的事情是接收一個列表和一個謂詞(predicate),過濾列表中不滿足謂詞的條目。換句話說,驗證這個方法正常工作的唯一方式就是事后檢查列表。這就像警察卧底,然后匯報她看到的一切。通常你不用測試替身也能做到這一點。這個例子中你可以詢問List對象,看它是否包含你所期望的條目。
至於測試替身——我們正在討論的測試間諜(簡稱Spy)——的方便之處在於,當沒有對象作為參數傳入時,通過它們的API也能揭示你想要了解的知識。代碼清單3.5顯示了這樣一個例子。
我們先來看看上述代碼清單中的場景。被測對象是一個分布式的日志對象DLog,代表了一組DLogTarget。當向DLog寫入時,你應該向所有DLogTarget寫入相同的消息。從測試的角度來看,事情有點尷尬,你無法知道指定的消息是否被寫入,因為DLogTarget接口只定義了一個方法write(),而且DLogTarget、ConsoleTarget和RemoteTarget的真實實現也都沒有提供任何方法。
測試間諜登場了。代碼清單3.6展示了一個精明的程序員如何鞭打他的女特工去干活。

這就是測試間諜的一切。像其他測試替身一樣,你將它們傳入。然后你令測試間諜記錄已發送的消息,並讓測試詢問測試間諜是否收到指定消息。干得漂亮!
簡而言之,測試間諜是一種測試替身,它用於記錄過去發生的情況,這樣測試在事后就知道所發生的一切。有時我們進一步利用這個概念,於是測試間諜就變成了全能的模擬對象。如果測試間諜像個卧底警察,那么模擬對象就像滲入暴民的遠程控制機器人。這可能需要一些解釋……
3.2.4 模擬對象反對驚喜
模擬對象(簡稱Mock)是特殊的Spy。它是一個在特定情景下可配置行為的對象。例如,UserRepository接口的模擬對象可能被告之:當帶着參數123調用findById()時要返回null,而當帶着參數124調用findById()時要返回User的一個實例。在這一點上,我們主要討論的是根據參數來對特定的方法調用打樁。
如果一旦任何意外發生時Mock就立即使測試失敗,Mock就能夠變得更加精確。例如,假設我們告訴了模擬對象如何應對帶着123或124的findById()調用,它就會嚴格按照指令工作。對於任何其他的調用——不論是調用不同的方法或者帶着另外的參數調用findById()——Mock就會拋出異常,直接使測試失敗。同樣,如果findById()被調用太多次,Mock就會抱怨——除非我們告訴它允許調用任意次數——如果預期的調用沒發生,Mock也會抱怨。
包括JMock、Mockito和EasyMock在內的模擬對象庫已經是成熟的工具了,崇尚測試的程序員可以借助它們獲得力量。每個庫都有自己的行事風格,但基本上你可以用它們中任何一個來完成所有的工作。
這並非模擬對象庫的全面教程,但是我們迅速看看代碼清單3.7中的例子,它展示了這種庫的具體用法。這里我們使用JMock,因為我碰巧有個項目正在使用JMock。

在這樣一小段測試代碼中,這個例子展示了許多模擬對象庫用法的典型構造。首先,我們告訴庫要為指定接口創建一個模擬對象。
在context.checking()中看似笨拙的代碼塊其實是測試在指導模擬的Internet,告訴它應該期待哪些交互,以及如何應對這些交互。這種情況下,我們預期測試會帶着包含"langpair=en%7Cfi"字符串的參數調用get()方法一次,對此,mock應當返回指定字符串。
最終,我們將Mock傳給被測的Translator對象,執行Translator,然后斷言Translator為我們的場景提供了正確的翻譯。
然而,這並非我們的全部斷言。如前所述,Mock可以嚴格地判斷已經發生的預期交互。在模擬Internet的例子中,Mock嚴格地斷言它確實收到了一次帶有指定子字符串參數的get()方法調用。
