鍍金池/ 教程/ iOS/ 置換測試: Mock, Stub 和其他
與四軸無人機的通訊
在沙盒中編寫腳本
結(jié)構體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學
NSString 與 Unicode
代碼簽名探析
測試
架構
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動開發(fā)
Collection View 動畫
截圖測試
MVVM 介紹
使 Mac 應用數(shù)據(jù)腳本化
一個完整的 Core Data 應用
插件
字符串
為 iOS 建立 Travis CI
先進的自動布局工具箱
動畫
為 iOS 7 重新設計 App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡應用實例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動畫解釋
響應式 Android 應用
初識 TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項目介紹
Swift 的強大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學習
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學習的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設計優(yōu)雅的移動游戲
繪制像素到屏幕上
相機與照片
音頻 API 一覽
交互式動畫
常見的后臺實踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應式視圖
Square Register 中的擴張
DTrace
基礎集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設計的藝術
導航應用
線程安全類的設計
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機項目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動追蹤
依賴注入
Swift
項目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對清單
View Controller 容器
學無止境
XCTest 測試實戰(zhàn)
iOS 7
Layer 中自定義屬性的動畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲
代碼審查的藝術:Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴展
理解 Scroll Views
使用 VIPER 構建 iOS 應用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請求
導入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機捕捉
語言標簽
同步案例學習
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識別
玩轉(zhuǎn)字符串
相機工作原理
Build 過程

置換測試: Mock, Stub 和其他

簡介

在理想情況下,你所做的所有測試都是能應對你實際代碼的高級測試。例如,UI 測試將模擬實際的用戶輸入(Klaas 在他的文章中有討論)等等。實但際上,這并非永遠都是個好主意。為每個測試用例都訪問一次數(shù)據(jù)庫或者旋轉(zhuǎn)一次 UI 會使你的測試跑得非常慢,這會降低你的生產(chǎn)力,并導致你不去經(jīng)常跑那些測試。若你測試的某段代碼依賴于網(wǎng)絡連接,這會要求你的測試環(huán)境具備網(wǎng)絡接入條件,而且這也難以模擬某些特殊的測試,比如當電話處于飛行模式情況下的時候。

正因如此,我們可以用一些模擬代碼替換你的實際代碼來編寫一些測試用例。

什么時候你會需要用到一些模擬 (mock) 對象呢?

讓我們從以下這些不同類型的模擬對象的基本定義開始。

double 可以理解為置換,它是所有模擬測試對象的統(tǒng)稱,我們也可以稱它為替身。一般來說,當你創(chuàng)建任意一種測試置換對象時,它將被用來替代某個指定類的對象。

stub 可以理解為測試樁,它能實現(xiàn)當特定的方法被調(diào)用時,返回一個指定的模擬值。如果你的測試用例需要一個伴生對象來提供一些數(shù)據(jù),可以使用 stub 來取代數(shù)據(jù)源,在測試設置時可以指定返回每次一致的模擬數(shù)據(jù)。

spy 可以理解為偵查,它負責匯報情況,持續(xù)追蹤什么方法被調(diào)用了,以及調(diào)用過程中傳遞了哪些參數(shù)。你能用它來實現(xiàn)測試斷言,比如一個特定的方法是否被調(diào)用或者是否使用正確的參數(shù)調(diào)用。當你需要測試兩個對象間的某些協(xié)議或者關系時會非常有用。

mock 與 spy 類似,但在使用上有些許不同。spy 追蹤所有的方法調(diào)用,并在事后讓你寫斷言,而 mock 通常需要你事先設定期望。你告訴它你期望發(fā)生什么,然后執(zhí)行測試代碼并驗證最后的結(jié)果與事先定義的期望是否一致。

fake 是一個具備完整功能實現(xiàn)和行為的對象,行為上來說它和這個類型的真實對象上一樣,但不同于它所模擬的類,它使測試變得更加容易。一個典型的例子是使用內(nèi)存中的數(shù)據(jù)庫來生成一個數(shù)據(jù)持久化對象,而不是去訪問一個真正的生產(chǎn)環(huán)境的數(shù)據(jù)庫。

實踐中,這些術語常常用起來不同于它們的定義,甚至可以互換。稍后我們在這篇文章中會看到一些庫,它們自認為自己是 "mock 對象框架",但是其實它們也提供 stub 的功能,而且驗證行為的方式也類似于我描述的 "spy" 而不是 "mock"。所以不要太過于陷入這些詞匯的細節(jié);我下這些定義更多的是因為要在高層次上區(qū)分這些概念,并且它對考慮不同類型測試對象的行為會有幫助。

如果你對不同類型的模擬測試對象更多的細節(jié)討論感興趣,Martin Fowler 的文章 "Mocks Aren't Stubs" 被認為是關于這個問題的權威討論。

模擬主義者 (Mockists) vs. 統(tǒng)計主義者 (Statists)

許多關于模擬對象的討論主要是衍生自 Fowler 的文章的,它們討論了兩種不同類型的程序員,模擬主義者和統(tǒng)計主義者,所寫的測試。

模擬主義的方式是測試對象之間的交互。通過使用模擬對象,你可以更容易地驗證被測對象是否遵循了它與其他類已建立的協(xié)議,使得在正確的時間發(fā)生正確的外部調(diào)用。對于那些使用行為驅(qū)動 (behavior-driven) 的開發(fā)者來說,這種測試可以驅(qū)動出更好的生產(chǎn)代碼,因為你需要明確模擬出特定的方法,這可以幫你設計出在兩個對象之間使用的更優(yōu)雅的API,這種想法與模擬驅(qū)動緊密聯(lián)系在一起。因此模擬主義的測試更偏向于單元級別的測試,而不是完全的端到端 (end-to-end) 測試。

統(tǒng)計主義的方式是不使用模擬對象。這種思路是測試時只測試狀態(tài)而不是行為,因此這種類型的測試更加健壯。使用模擬測試時,如果你更新了實際類的行為,模擬類也需要同步更新;如果你忘了這么做,你可能會遇到測試可以通過但是代碼卻不能正確工作的情況。通過強調(diào)在測試環(huán)境中只使用那些真正的代碼,統(tǒng)計主義的測試可以幫助你減少測試代碼和實現(xiàn)代碼的耦合度,并降低出錯率。這種類型的測試,您可能已經(jīng)猜到,適合于更全面的端到端的測試。

當然,并不是說有兩個對立的程序員學派;你不可能看到模擬主義和統(tǒng)計主義的當街對決。這種分歧是有用的,但是,得認識到 mock 在有些時候是你的工具箱里最好的工具,但是有時候又不是。不同類型的測試適用于不同的任務,并且最高效的測試套件往往是不同測試風格的集合體。仔細考慮你到底想要用單個測試來驗證些什么,這能幫助你找到最合適的測試方式,而且能幫你決定對于當前工作來說,使用模擬測試對象是否是正確的工具。

深入代碼

理論上談起來所有一切都沒什么問題,但讓我們來看一個你需要用到 mock 的真實用例。

讓我們試著測試一個對象,它上面有一個方法,是通過調(diào)用 UIApplicationopenURL: 方法來打開另外一個應用程序。(這是我在測試我的 IntentKit 庫時遇到的一個真實問題。) 給這個用例寫一個端到端的測試,就算是有可能做到,也是非常困難的,因為 '成功狀態(tài)' 本身導致了應用程序的關閉。自然的選擇是,模擬出一個 UIApplication 對象,并驗證這個模擬對象是否確實調(diào)用了 openURL 方法打開正確的 URL。

假設這個對象有這樣的方法:

@interface AppLinker : NSObject
        - (instancetype)initWithApplication:(UIApplication *)application;
        - (void)doSomething:(NSURL *)url;
@end

這是一個非常牽強的例子,但是請容忍我一下。在這個例子中,你會注意到我們使用了構造方法進行注入,當我們創(chuàng)建 AppLinker 的對象時將 UIApplication 對象注入到其中。大部分情況下,使用模擬對象要求使用某種形式的依賴注入。如果這個概念對你很陌生,請一定看看本期的 Jon 的文章 中的描述。

OCMockito

OCMockito 是一個非常輕量級的使用模擬對象的庫:

UIApplication *app = mock([UIApplication class]);
AppLinker *linker = [AppLinker alloc] initWithApplication:app];
NSURL *url = [NSURL urlWithString:@"https://google.com"];

[linker doSomething:URL];

[verify(app) openURL:url];

OCMock

OCMock 是另一個 Objective-C 的模擬對象庫。和 OCMockito 類似,它提供了關于 stub 和 mock 的所有功能,并且包括了你可能需要的一切功能。它比 OCMockito 的功能更強,依賴于你的個人選擇,各有利弊。

在最基本層面上,我們可以使用 OCMock 來重寫出與之前非常類似的測試:

id app = OCMClassMock([UIApplication class]);
AppLinker *linker = [AppLinker alloc] initWithApplication:app];
NSURL *url = [NSURL urlWithString:@"https://google.com"];

[linker doSomething:url];

OCMVerify([app openURL:url]);

這種在你測試后再驗證調(diào)用方法的模擬測試風格被認為是一種 “運行后驗證” 的方式。OCMock 只在最近 3.0 版本后增加了對該功能的支持。同時它也支持老版本的風格,即對期望運行的驗證,在執(zhí)行測試代碼前先設定對測試結(jié)果的期望。最后,你只需要驗證期望和實際結(jié)果是否對應:

id app = OCMClassMock([UIApplication class]);

AppLinker *linker = [AppLinker alloc] initWithApplication:app];
NSURL *url = [NSURL urlWithString:@"https://google.com"];

OCMExpect([app openURL:url]);

[linker doSomething:url];

OCMVerifyAll();

由于 OCMock 也支持對類方法的 stub,你也可以用這種方式來測試,如果 doSomething 方法通過 [UIApplication sharedApplication] 來實現(xiàn)而不是 UIApplication 對象的注入初始化:

id app = OCMClassMock([UIApplication class]);
OCMStub([app sharedInstance]).andReturn(app);

AppLinker *linker = [AppLinker alloc] init];
NSURL *url = [NSURL urlWithString:@"https://google.com"];

[linker doSomething:url];

OCMVerify([app openURL:url]);

你會發(fā)現(xiàn) stub 類方法和 stub 實例方法看起來是一樣的。

構建你自己的測試

對于像這種簡單的用例,你也許不需要這么重量級的模擬對象測試庫。通常,你只需要創(chuàng)建你自己的模擬對象來測試你關心的行為:

@interface FakeApplication : NSObject
    @property (readwrite, nonatomic, strong) NSURL *lastOpenedURL;

    - (void)openURL:(NSURL *)url;
@end

@implementation FakeApplication
    - (void)openURL:(NSURL *)url {
        self.lastOpenedURL = url;
    }
@end

以下是測試:

FakeApplication *app = [[FakeApplication alloc] init];
AppLinker *linker = [AppLinker alloc] initWithApplication:app];
NSURL *url = [NSURL urlWithString:@"https://google.com"];

[linker doSomething:url];

XCAssertEqual(app.lastOpenedURL, url, @"Did not open the expected URL");

對于類似這個已經(jīng)設計好的例子,就可能會出現(xiàn)這種情況,創(chuàng)造你自己的模擬對象只是增加了很多不必要的樣板,但如果你覺得需要模擬更為復雜的對象交互,那么完全控制模擬對象的行為就會非常有價值。

使用哪一個?

選擇哪一種方案完全依賴于你的具體測試情況以及你的個人偏好。OCMockito 和 OCMock 都可以通過 CocoaPods 安裝,將它們集成到你現(xiàn)有的測試環(huán)境都非常簡單,但需要注意的是,除非你需要,否則避免新增一些其他的依賴。另外除非真的需要,最好就都創(chuàng)建一些簡單的模擬對象。

模擬測試時的注意事項

在任何形式的測試中你有可能碰到的最大的問題之一是寫的測試和實現(xiàn)代碼耦合過于緊密。測試中一個最重要的關鍵點是降低未來的變化所帶來的成本;如果改變代碼的實現(xiàn)細節(jié)破壞了當前的測試,則這種成本已經(jīng)增加了。也就是說,其實為了最小化由于使用模擬測試所造成不利影響,其實你有很多可以做的。

依賴注入是你的好伙伴

如果你還沒有使用依賴注入,或許你會需要它。雖然有時候不使用依賴注入來模擬對象也是可以的的 (比如以上面使用 OCMock 模擬類方法),但是通常是不太可能的。即使可能,設置測試所引入的復雜度也可能大于它能帶來的好處。如果你使用依賴注入的話,你會發(fā)現(xiàn)使用 stub 和 mock 方式寫測試要容易的多。

不要模擬你沒有的

許多有經(jīng)驗的測試人員都會警告你“不要模擬你沒有的東西”,意思是你應該只為你代碼庫本身擁有的對象創(chuàng)建 mock 或 stub,而不是為第三方依賴或一些庫去創(chuàng)建。這里主要有兩個原因,一個是基于實際情況的,一個是更具有哲學性的考慮。

對于你的代碼庫,你對它不同接口的穩(wěn)定性和不穩(wěn)定性大概會有一個感覺,所以你可以通過你的直覺來判斷使用替換測試的方法是不是可能會導致測試過于脆弱。一般來說,你對第三方代碼沒有這樣的把握。為了解決這個問題,一個通用的做法是為第三方代碼創(chuàng)建包裝類來抽象出它的行為。在某些情況下,僅僅是轉(zhuǎn)移復雜性而不是降低復雜性往往是沒什么意義的。但是在一些情況下,你會很經(jīng)常使用你的第三方代碼,這時這就是一個精簡你測試的好方法。你的單元測試能模擬出自定義對象,并使用高層次的集成或功能測試來測試你的包裝類本身。

iOS 和 OS X 開發(fā)世界的唯一性導致了事情稍微復雜一些。我們做的很多事情都依賴于 Apple 的框架,這個框架遠遠超過了其他語言的一些標準庫。雖然 NSUserDefaults 不是一個“你擁有”對象,但是,如果你發(fā)現(xiàn)你有需要把它模擬出來,那就放心去做吧,蘋果不太可能會在未來的 Xcode 的版本中推出打破這個 API 的變化。

另一個不要模擬第三方依賴庫的原因更具哲學性。使用模擬主義風格書寫測試的部分原因是通過這樣的測試能比較容易的找到兩個對象間最清晰可行的接口。但是如果是第三方依賴,你無法對其進行控制;API 協(xié)議中的一些詳細信息已經(jīng)被第三方庫定死了,所以你無法通過測試來通過實驗有效地驗證接口是否有改進的余地。這本身不是問題,但在很多情況下,它降低了模擬測試的效果,直到把模擬測試的優(yōu)點抹殺殆盡。

不要模仿我!

測試沒有銀彈;基于你的個人傾向和代碼的具體特性,不同的情況下需要使用不同的策略。測試替身可能不適用所有的情況,但它們會是你測試工具箱中一個非常有效的工具。不管你傾向于使用框架在單元測試中模擬出一切,還是只是根據(jù)需要創(chuàng)建你自己的模擬對象,當你思考如何測試你的代碼時,牢記模擬對象是非常有意義。