在理想情況下,你所做的所有測(cè)試都是能應(yīng)對(duì)你實(shí)際代碼的高級(jí)測(cè)試。例如,UI 測(cè)試將模擬實(shí)際的用戶輸入(Klaas 在他的文章中有討論)等等。實(shí)但際上,這并非永遠(yuǎn)都是個(gè)好主意。為每個(gè)測(cè)試用例都訪問一次數(shù)據(jù)庫或者旋轉(zhuǎn)一次 UI 會(huì)使你的測(cè)試跑得非常慢,這會(huì)降低你的生產(chǎn)力,并導(dǎo)致你不去經(jīng)常跑那些測(cè)試。若你測(cè)試的某段代碼依賴于網(wǎng)絡(luò)連接,這會(huì)要求你的測(cè)試環(huán)境具備網(wǎng)絡(luò)接入條件,而且這也難以模擬某些特殊的測(cè)試,比如當(dāng)電話處于飛行模式情況下的時(shí)候。
正因如此,我們可以用一些模擬代碼替換你的實(shí)際代碼來編寫一些測(cè)試用例。
讓我們從以下這些不同類型的模擬對(duì)象的基本定義開始。
double 可以理解為置換,它是所有模擬測(cè)試對(duì)象的統(tǒng)稱,我們也可以稱它為替身。一般來說,當(dāng)你創(chuàng)建任意一種測(cè)試置換對(duì)象時(shí),它將被用來替代某個(gè)指定類的對(duì)象。
stub 可以理解為測(cè)試樁,它能實(shí)現(xiàn)當(dāng)特定的方法被調(diào)用時(shí),返回一個(gè)指定的模擬值。如果你的測(cè)試用例需要一個(gè)伴生對(duì)象來提供一些數(shù)據(jù),可以使用 stub 來取代數(shù)據(jù)源,在測(cè)試設(shè)置時(shí)可以指定返回每次一致的模擬數(shù)據(jù)。
spy 可以理解為偵查,它負(fù)責(zé)匯報(bào)情況,持續(xù)追蹤什么方法被調(diào)用了,以及調(diào)用過程中傳遞了哪些參數(shù)。你能用它來實(shí)現(xiàn)測(cè)試斷言,比如一個(gè)特定的方法是否被調(diào)用或者是否使用正確的參數(shù)調(diào)用。當(dāng)你需要測(cè)試兩個(gè)對(duì)象間的某些協(xié)議或者關(guān)系時(shí)會(huì)非常有用。
mock 與 spy 類似,但在使用上有些許不同。spy 追蹤所有的方法調(diào)用,并在事后讓你寫斷言,而 mock 通常需要你事先設(shè)定期望。你告訴它你期望發(fā)生什么,然后執(zhí)行測(cè)試代碼并驗(yàn)證最后的結(jié)果與事先定義的期望是否一致。
fake 是一個(gè)具備完整功能實(shí)現(xiàn)和行為的對(duì)象,行為上來說它和這個(gè)類型的真實(shí)對(duì)象上一樣,但不同于它所模擬的類,它使測(cè)試變得更加容易。一個(gè)典型的例子是使用內(nèi)存中的數(shù)據(jù)庫來生成一個(gè)數(shù)據(jù)持久化對(duì)象,而不是去訪問一個(gè)真正的生產(chǎn)環(huán)境的數(shù)據(jù)庫。
實(shí)踐中,這些術(shù)語常常用起來不同于它們的定義,甚至可以互換。稍后我們?cè)谶@篇文章中會(huì)看到一些庫,它們自認(rèn)為自己是 “mock 對(duì)象框架”,但是其實(shí)它們也提供 stub 的功能,而且驗(yàn)證行為的方式也類似于我描述的 “spy” 而不是 “mock”。所以不要太過于陷入這些詞匯的細(xì)節(jié);我下這些定義更多的是因?yàn)橐诟邔哟紊蠀^(qū)分這些概念,并且它對(duì)考慮不同類型測(cè)試對(duì)象的行為會(huì)有幫助。
如果你對(duì)不同類型的模擬測(cè)試對(duì)象更多的細(xì)節(jié)討論感興趣,Martin Fowler 的文章 “Mocks Aren’t Stubs” 被認(rèn)為是關(guān)于這個(gè)問題的權(quán)威討論。
許多關(guān)于模擬對(duì)象的討論主要是衍生自 Fowler 的文章的,它們討論了兩種不同類型的程序員,模擬主義者和統(tǒng)計(jì)主義者,所寫的測(cè)試。
模擬主意的方式是測(cè)試對(duì)象之間的交互。通過使用模擬對(duì)象,你可以更容易地驗(yàn)證被測(cè)對(duì)象是否遵循了它與其他類已建立的協(xié)議,使得在正確的時(shí)間發(fā)生正確的外部調(diào)用。對(duì)于那些使用行為驅(qū)動(dòng) (behavior-driven) 的開發(fā)者來說,這種測(cè)試可以驅(qū)動(dòng)出更好的生產(chǎn)代碼,因?yàn)槟阈枰鞔_模擬出特定的方法,這可以幫你設(shè)計(jì)出在兩個(gè)對(duì)象之間使用的更優(yōu)雅的API,這種想法與模擬驅(qū)動(dòng)緊密聯(lián)系在一起。因此模擬主義的測(cè)試更偏向于單元級(jí)別的測(cè)試,而不是完全的端到端 (end-to-end) 測(cè)試。
統(tǒng)計(jì)主義的方式是不使用模擬對(duì)象。這種思路是測(cè)試時(shí)只測(cè)試狀態(tài)而不是行為,因此這種類型的測(cè)試更加健壯。使用模擬測(cè)試時(shí),如果你更新了實(shí)際類的行為,模擬類也需要同步更新;如果你忘了這么做,你可能會(huì)遇到測(cè)試可以通過但是代碼卻不能正確工作的情況。通過強(qiáng)調(diào)在測(cè)試環(huán)境中只使用那些真正的代碼,統(tǒng)計(jì)主意的測(cè)試可以幫助你減少測(cè)試代碼和實(shí)現(xiàn)代碼的耦合度,并降低出錯(cuò)率。這種類型的測(cè)試,您可能已經(jīng)猜到,適合于更全面的端到端的測(cè)試。
當(dāng)然,并不是說有兩個(gè)對(duì)立的程序員學(xué)派;你不可能看到模擬主義和統(tǒng)計(jì)主義的當(dāng)街對(duì)決。這種分歧是有用的,但是,得認(rèn)識(shí)到 mock 在有些時(shí)候是你的工具箱里最好的工具,但是有時(shí)候又不是。不同類型的測(cè)試適用于不同的任務(wù),并且最高效的測(cè)試套件往往是不同測(cè)試風(fēng)格的集合體。仔細(xì)考慮你到底想要用單個(gè)測(cè)試來驗(yàn)證些什么,這能幫助你找到最合適的測(cè)試方式,而且能幫你決定對(duì)于當(dāng)前工作來說,使用模擬測(cè)試對(duì)象是否是正確的工具。
理論上談起來所有一切都沒什么問題,但讓我們來看一個(gè)你需要用到 mock 的真實(shí)用例。
讓我們?cè)囍鴾y(cè)試一個(gè)對(duì)象,它上面有一個(gè)方法,是通過調(diào)用 UIapplication
的 openURL:
方法來打開另外一個(gè)應(yīng)用程序。(這是我在測(cè)試我的 IntentKit 庫時(shí)遇到的一個(gè)真實(shí)問題。) 給這個(gè)用例寫一個(gè)端到端的測(cè)試,就算是有可能做到,也是非常困難的,因?yàn)?‘成功狀態(tài)’ 本身導(dǎo)致了應(yīng)用程序的關(guān)閉。自然的選擇是,模擬出一個(gè) UIApplication
對(duì)象,并驗(yàn)證這個(gè)模擬對(duì)象是否確實(shí)調(diào)用了 openURL
方法打開正確的 URL。
假設(shè)這個(gè)對(duì)象有這樣的方法:
@interface AppLinker : NSObject - (instancetype)initWithApplication:(UIApplication *)application; - (void)doSomething:(NSURL *)url;@end
這是一個(gè)非常牽強(qiáng)的例子,但是請(qǐng)容忍我一下。在這個(gè)例子中,你會(huì)注意到我們使用了構(gòu)造方法進(jìn)行注入,當(dāng)我們創(chuàng)建 AppLinker
的對(duì)象時(shí)將 UIApplication
對(duì)象注入到其中。大部分情況下,使用模擬對(duì)象要求使用某種形式的依賴注入。如果這個(gè)概念對(duì)你很陌生,請(qǐng)一定看看本期的 Jon 的文章 中的描述。
OCMockito 是一個(gè)非常輕量級(jí)的使用模擬對(duì)象的庫:
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 是另一個(gè) Objective-C 的模擬對(duì)象庫。和 OCMockito 類似,它提供了關(guān)于 stub 和 mock 的所有功能,并且包括了你可能需要的一切功能。它比 OCMockito 的功能更強(qiáng),依賴于你的個(gè)人選擇,各有利弊。
在最基本層面上,我們可以使用 OCMock 來重寫出與之前非常類似的測(cè)試:
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]);
這種在你測(cè)試后再驗(yàn)證調(diào)用方法的模擬測(cè)試風(fēng)格被認(rèn)為是一種 “運(yùn)行后驗(yàn)證” 的方式。OCMock 只在最近 3.0 版本后增加了對(duì)該功能的支持。同時(shí)它也支持老版本的風(fēng)格,即對(duì)期望運(yùn)行的驗(yàn)證,在執(zhí)行測(cè)試代碼前先設(shè)定對(duì)測(cè)試結(jié)果的期望。最后,你只需要驗(yàn)證期望和實(shí)際結(jié)果是否對(duì)應(yīng):
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();
Because OCMock lets you stub out class methods, you could also test this using OCMock, if your implementation of doSomething
uses [UIApplication sharedApplication]
rather than the UIApplication
object injected in the initializer: 由于 OCMock 也支持對(duì)類方法的 stub,你也可以用這種方式來來測(cè)試,如果 doSomething
方法通過 [UIApplication sharedApplication]
來實(shí)現(xiàn)而不是 UIApplication
對(duì)象的注入初始化:
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]);
你會(huì)發(fā)現(xiàn) stub 類方法和 stub 實(shí)例方法看起來是一樣的。
對(duì)于像這種簡(jiǎn)單的用例,你也許不需要這么重量級(jí)的模擬對(duì)象測(cè)試庫。通常,你只需要?jiǎng)?chuàng)建你自己的模擬對(duì)象來測(cè)試你關(guān)心的行為:
@interface FakeApplication : NSObject @PRoperty (readwrite, nonatomic, strong) NSURL *lastOpenedURL; - (void)openURL:(NSURL *)url;@end@implementation FakeApplication - (void)openURL:(NSURL *)url { self.lastOpenedURL = url; }@end
以下是測(cè)試:
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");
對(duì)于類似這個(gè)已經(jīng)設(shè)計(jì)好的例子,就可能會(huì)出現(xiàn)這種情況,創(chuàng)造你自己的模擬對(duì)象只是增加了很多不必要的樣板,但如果你覺得需要模擬更為復(fù)雜的對(duì)象交互,那么完全控制模擬對(duì)象的行為就會(huì)非常有價(jià)值。
選擇哪一種方案完全依賴于你的具體測(cè)試情況以及你的個(gè)人偏好。OCMockito 和 OCMock 都可以通過 CocoaPods 安裝,將它們集成到你現(xiàn)有的測(cè)試環(huán)境都非常簡(jiǎn)單,但需要注意的是,除非你需要,否則避免新增一些其他的依賴。另外除非真的需要,最好就都創(chuàng)建一些簡(jiǎn)單的模擬對(duì)象。
在任何形式的測(cè)試中你有可能碰到的最大的問題之一是寫的測(cè)試和實(shí)現(xiàn)代碼耦合過于緊密。測(cè)試中一個(gè)最重要的關(guān)鍵點(diǎn)是降低未來的變化所帶來的成本;如果改變代碼的實(shí)現(xiàn)細(xì)節(jié)破壞了當(dāng)前的測(cè)試,則這種成本已經(jīng)增加了。也就是說,其實(shí)為了最小化由于使用模擬測(cè)試所造成不利影響,其實(shí)你有很多可以做的。
如果你還沒有使用依賴注入,或許你會(huì)需要它。雖然有時(shí)候不使用依賴注入來模擬對(duì)象也是可以的的 (比如以上面使用 OCMock 模擬類方法),但是通常是不太可能的。即使可能,設(shè)置測(cè)試所引入的復(fù)雜度也可能大于它能帶來的好處。如果你使用依賴注入的話,你會(huì)發(fā)現(xiàn)使用 stub 和 mock 方式寫測(cè)試要容易的多。
許多有經(jīng)驗(yàn)的測(cè)試人員都會(huì)警告你“不要模擬你沒有的東西”,意思是你應(yīng)該只為你代碼庫本身擁有的對(duì)象創(chuàng)建 mock 或 stub,而不是為第三方依賴或一些庫去創(chuàng)建。這里主要有兩個(gè)原因,一個(gè)是基于實(shí)際情況的,一個(gè)是更具有哲學(xué)性的考慮。
對(duì)于你的代碼庫,你對(duì)它不同接口的穩(wěn)定性和不穩(wěn)定性大概會(huì)有一個(gè)感覺,所以你可以通過你的直覺來判斷使用替換測(cè)試的方法是不是可能會(huì)導(dǎo)致測(cè)試過于脆弱。一般來說,你對(duì)第三方代碼沒有這樣的把握。為了解決這個(gè)問題,一個(gè)通用的做法是為第三方代碼創(chuàng)建包裝類來抽象出它的行為。在某些情況下,僅僅是轉(zhuǎn)移復(fù)雜性而不是降低復(fù)雜性往往是沒什么意義的。但是在一些情況下,你會(huì)很經(jīng)常使用你的第三方代碼,這時(shí)這就是一個(gè)精簡(jiǎn)你測(cè)試的好方法。你的單元測(cè)試能模擬出自定義對(duì)象,并使用高層次的集成或功能測(cè)試來測(cè)試你的包裝類本身。
iOS 和 OS X 開發(fā)世界的唯一性導(dǎo)致了事情稍微復(fù)雜一些。我們做的很多事情都依賴于 Apple 的框架,這個(gè)框架遠(yuǎn)遠(yuǎn)超過了其他語言的一些標(biāo)準(zhǔn)庫。雖然 NSUserDefaults
不是一個(gè)“你擁有”對(duì)象,但是,如果你發(fā)現(xiàn)你有需要把它模擬出來,那就放心去做吧,蘋果不太可能會(huì)在未來的 Xcode 的版本中推出打破這個(gè) API 的變化。
另一個(gè)不要模擬第三方依賴庫的原因更具哲學(xué)性。使用模擬主義風(fēng)格書寫測(cè)試的部分原因是通過這樣的測(cè)試能比較容易的找到兩個(gè)對(duì)象間最清晰可行的接口。但是如果是第三方依賴,你無法對(duì)其進(jìn)行控制;API 協(xié)議中的一些詳細(xì)信息已經(jīng)被第三方庫定死了,所以你無法通過測(cè)試來通過實(shí)驗(yàn)有效地驗(yàn)證接口是否有改進(jìn)的余地。這本身不是問題,但在很多情況下,它降低了模擬測(cè)試的效果,直到把模擬測(cè)試的優(yōu)點(diǎn)抹殺殆盡。
測(cè)試沒有銀彈;基于你的個(gè)人傾向和代碼的具體特性,不同的情況下需要使用不同的策略。測(cè)試替身可能不適用所有的情況,但它們會(huì)是你測(cè)試工具箱中一個(gè)非常有效的工具。不管你傾向于使用框架在單元測(cè)試中模擬出一切,還是只是根據(jù)需要?jiǎng)?chuàng)建你自己的模擬對(duì)象,當(dāng)你思考如何測(cè)試你的代碼時(shí),牢記模擬對(duì)象是非常有意義。
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注