iOS單元測試[OCMock常見使用方式]

該文章使用的API是OCMock老版本的API,新版本也兼容老版本的API,譯者在用到老版本的API處已經(jīng)添加了對(duì)應(yīng)的新版本(OCMock3)的API供讀者參考。
愛好者
這篇文章假設(shè)讀者都能熟悉使用Xcode的測試框架XCTest,或者BBD測試工具Kiwi或其他的iOS測試框架
什么是mock?差不多就是紙老虎
當(dāng)我們寫單元測試的時(shí)候,不可避免的要去盡可能少的實(shí)例化一些具體的組件來保持測試既短又快。而且保持單元的隔離。在現(xiàn)代的面向?qū)ο笙到y(tǒng)中,測試的組件很可能會(huì)有幾個(gè)依賴的對(duì)象。我們用mock來替代實(shí)例化具體的依賴class。mock是在測試中的一個(gè)偽造的有預(yù)定義行為的具體對(duì)象的替身對(duì)象。被測試的組件不知道其中的差異!你的組件是在一個(gè)更大的系統(tǒng)中被設(shè)計(jì)的,你可以很有信心的用mock來測試你的組件。
常見的mock使用案例
stub方法
我們用一個(gè)簡單的例子來開始解釋OCMock中一般的stub語法。

id jalopy = [OCMock mockForClass[Car class]];
 [[[jalopy stub] andReturn:@"75kph"] goFaster:[OCMArg any] units:@"kph"]; 
// if returning a scalar value, andReturnValue: can be used

OCMock3 新版本對(duì)應(yīng)API

id jalopy = OCMStrictClassMock([Car class]);
OCMStub([jalopy goFaster:[OCMArg any] units:@"kph"]).andReturn(@"75kph");
// if returning a scalar value, andReturnValue: can be used

這個(gè)簡單的例子首先從Car類中mock出一個(gè)jalopy(老爺車),然后,stub掉goFaster:方法讓它返回字符串@”75kph”。stub語法可能看起來有點(diǎn)奇怪,但這是普遍的做法:
ourMockObject stub] whatItShouldReturn ] method:

OCMock3 新版本對(duì)應(yīng)API
OCMStub([ourMockObject method:]).andReturn()

一個(gè)非常重要的說明:注意[OCMArg any]的用法。當(dāng)指定一個(gè)帶參數(shù)的方法時(shí),方法被調(diào)用并且參數(shù)為指定參數(shù)的話,mock會(huì)返回andReturn:指定的值。[OCMArg any]
方法告訴stub匹配所有的參數(shù)值。舉個(gè)例子:
[car goFaster:84 units:@"mph"];

不會(huì)觸發(fā)stub,因?yàn)樽詈笠粋€(gè)參數(shù)不匹配”kph”.
類方法
OCMock會(huì)在mock實(shí)例上沒有找到相同名字的實(shí)例方法的時(shí)候去找同名的類方法。在名字相同的情況下(類方法和實(shí)例方法同名),用classMethod
來指定類方法:

[[[[jalopy stub] classMethod] andReturn:@"expired"] checkWarrany];

在OCMock3中classMethod和instanceMethod的stub方式一樣,例如:

id classMock = OCMClassMock([SomeClass class]);
OCMStub([classMock aClassMethod]).andReturn(@"Test string");
// result is @"Test string"
NSString *result = [SomeClass aClassMethod];

mock類型 – niceMock,partialMock
OCMock提供了幾種不同類型的mock,每個(gè)都有他們特定的使用場景。
用這種方式來創(chuàng)建任意mock:

id mockThing = [OCMock mockForClass[Thing class]];

OCMock3 新版本對(duì)應(yīng)API

id mockThing = OCMStrictClassMock([Thing class]);

這就是我所說的‘vanilla’ mock。‘vanilla’ mock當(dāng)調(diào)用一個(gè)沒有stub的方法的時(shí)候會(huì)拋出一個(gè)異常。這會(huì)得到一個(gè)單調(diào)的mock,且在mock的生命周期中每一個(gè)方法調(diào)用都要被stub掉。(更多信息請(qǐng)看下一節(jié)關(guān)于stub)如果你不想stub很多方法,用‘nice’ mock。‘nice’ mock非常有禮貌而且不會(huì)在一個(gè)沒有stub掉的方法被調(diào)用的時(shí)候拋出異常。

id niceMockThing = [OCMock niceMockForClass[Thing class]];

OCMock3 新版本對(duì)應(yīng)API

id mockThing = OCMClassMock([Thing class]);

最后一個(gè)mock類型是‘partial’ mock。當(dāng)一個(gè)沒有stub掉的方法被調(diào)用了,這個(gè)方法會(huì)被轉(zhuǎn)發(fā)到真實(shí)的對(duì)象上。這是對(duì)mock技術(shù)上的欺騙,但是非常有用,當(dāng)有一些類不適合讓自己很好的被stub。

Thing *someThing = [Thing alloc] init];
id aMock = [OCMockObject partialMockForObject:someThing]

OCMock3 新版本對(duì)應(yīng)API

Thing *someThing = [Thing alloc] init];
id aMock = OCMPartialMock(someThing);

驗(yàn)證方法是否被調(diào)用
驗(yàn)證方法是否被調(diào)用非常簡單。這個(gè)可以用expect
來完成拒絕和驗(yàn)證方法:

id niceMockThing = [OCMock niceMockForClass[Thing class]];
 [[niceMockThing expect] greeting:@"hello"]; 
// verify the method was called as expected
 [niceMocking verify];

OCMock3 新版本對(duì)應(yīng)API

id niceMockThing = OCMClassMock([Thing class]);
OCMVerify([niceMockThing greeting:@"hello"]);

當(dāng)被驗(yàn)證的方法沒有被調(diào)用的時(shí)候會(huì)拋出異常。如果你用的是XCTest,那么請(qǐng)用XCTAssertNotThrow
來包裝驗(yàn)證調(diào)用。拒絕方法調(diào)用也是同樣的道理,但是會(huì)再方法調(diào)用的時(shí)候拋出異常。就像stub,selector和傳遞過去驗(yàn)證的參數(shù)必須匹配調(diào)用時(shí)候傳遞過去的參數(shù)。用[OCMArg any]
可以簡化我們的工作。
處理block參數(shù)
OCMock也可以處理block回調(diào)參數(shù)。block回調(diào)通常用于網(wǎng)絡(luò)代碼,數(shù)據(jù)庫代碼,或者在任何異步操作中。在這個(gè)例子中,思考下下面的方法:

- (void)downloadWeatherDataForZip:(NSString *)zip callback:(void (^)(NSDictionary *response))callback;

在這個(gè)例子中,我們有一個(gè)下載天氣壓縮數(shù)據(jù)的方法,并且把下載下來的dictionary代理到一個(gè)block的回調(diào)中。在測試中,我們通過預(yù)定義的天氣數(shù)據(jù)來測試回調(diào)處理。這也是明智的測試失敗場景。你永遠(yuǎn)不會(huì)知道網(wǎng)絡(luò)上會(huì)返回你什么東西!

// 1. stub using 
OCMock andDo: operator.[[[groupModelMock stub] andDo:^(NSInvocation *invoke) { 
//2. declare a block with same signature 
void (^weatherStubResponse)(NSDictionary *dict); 
//3. link argument 3 with with our block callback 
[invoke getArgument:&weatherStubResponse atIndex:3]; 
//4. invoke block with pre-defined input 
NSDictionary *testResponse = @{@"high": 43 , @"low": 12}; 
weatherStubResponse(groupMemberMock); }]
downloadWeatherDataForZip@"80304" callback:[OCMArg any] ];

OCMock3 新版本對(duì)應(yīng)API

// 1. stub using OCMock andDo:operator.
OCMStub([groupModelMock downloadWeatherDataForZip:@"80304" callback:[OCMArg any]]]).andDo(^(NSInvocation *invocation){ 
//2. declare a block with same signature 
void (^weatherStubResponse)(NSDictionary *dict);
 //3. link argument 3 with with our block callback 
[invoke getArgument:&weatherStubResponse atIndex:3]; //4. invoke block with pre-defined input 
NSDictionary *testResponse = @{@"high": 43 , @"low": 12}; 
weatherStubResponse(groupMemberMock); });

這里的大體思想相當(dāng)簡單,即便如此,他的實(shí)現(xiàn)也需要一些說明:
1.這個(gè)mock對(duì)象使用帶NSInvocation參數(shù)的“andDo”方法。一個(gè)NSInvocation對(duì)象代表一個(gè)‘objectivetified’(實(shí)在不知道這個(gè)什么鬼)表現(xiàn)的方法調(diào)用。通過這個(gè)NSinvocation對(duì)象,使得攔截傳遞給我們的方法的block參數(shù)變得可能。2.用與我們測試的方法中相同的方法簽名聲明一個(gè)block參數(shù)。3.NSInvocation實(shí)例方法"getArgument:atIndex:"將賦值后的塊函數(shù)傳遞都原始函數(shù)中定義的塊函數(shù)中。注意:在Objective-C中,傳遞給任意方法的前兩個(gè)參數(shù)都是“self”和“_cmd”.這是一個(gè)運(yùn)行時(shí)的小功能以及用下標(biāo)來獲取NSInvocation參數(shù)時(shí)我們需要考慮的東西。4.最后,傳遞這個(gè)回調(diào)的預(yù)定義字典。

最后
希望這篇文章和例子已經(jīng)陳述清楚一些OCMock最通用的用法。
OCMock站點(diǎn):http://ocmock.org/features/是一個(gè)最好的學(xué)習(xí)OCMock的地方。
mock是單調(diào)的但是對(duì)于一個(gè)現(xiàn)代的OO系統(tǒng)卻是必須的。如果一個(gè)依賴圖很難用mock來測試,這個(gè)跡象表明你的設(shè)計(jì)需要重新考慮了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評(píng)論 6 543
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,520評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,362評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,541評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,896評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評(píng)論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,062評(píng)論 0 290
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,608評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,356評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,555評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評(píng)論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,769評(píng)論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評(píng)論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,289評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,516評(píng)論 2 379

推薦閱讀更多精彩內(nèi)容