iOS單元測試:譯-OCMock常見使用方式
該文章使用的API是OCMock老版本的API,新版本也兼容老版本的API,譯者在用到老版本的API處已經(jīng)添加了對應(yīng)的新版本(OCMock3)的API供讀者參考。
愛好者
這篇文章假設(shè)讀者都能熟悉使用Xcode5的測試框架XCTest,或者BBD測試工具Kiwi或其他的iOS測試框架
什么是mock?差不多就是紙老虎
當(dāng)我們寫單元測試的時候,不可避免的要去盡可能少的實(shí)例化一些具體的組件來保持測試既短又快。而且保持單元的隔離。在現(xiàn)代的面向?qū)ο笙到y(tǒng)中,測試的組件很可能會有幾個依賴的對象。我們用mock來替代實(shí)例化具體的依賴class。mock是在測試中的一個偽造的有預(yù)定義行為的具體對象的替身對象。被測試的組件不知道其中的差異!你的組件是在一個更大的系統(tǒng)中被設(shè)計的,你可以很有信心的用mock來測試你的組件。
常見的mock使用案例
stub方法
我們用一個簡單的例子來開始解釋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 新版本對應(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
這個簡單的例子首先從Car類中mock出一個jalopy(老爺車),然后,stub掉goFaster:方法讓它返回字符串@”75kph”。stub語法可能看起來有點(diǎn)奇怪,但這是普遍的做法:
- ourMockObject stub] whatItShouldReturn ] method:
OCMock3 新版本對應(yīng)API
- OCMStub([ourMockObject method:]).andReturn()
一個非常重要的說明:注意[OCMArg any]的用法。當(dāng)指定一個帶參數(shù)的方法時,方法被調(diào)用并且參數(shù)為指定參數(shù)的話,mock會返回andReturn:指定的值。[OCMArg any]方法告訴stub匹配所有的參數(shù)值。舉個例子:
- [car goFaster:84 units:@"mph"];
不會觸發(fā)stub,因?yàn)樽詈笠粋€參數(shù)不匹配”kph”.
類方法
OCMock會在mock實(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,每個都有他們特定的使用場景。
用這種方式來創(chuàng)建任意mock:
- id mockThing = [OCMock mockForClass[Thing class]];
OCMock3 新版本對應(yīng)API
- id mockThing = OCMStrictClassMock([Thing class]);
這就是我所說的‘vanilla’ mock。‘vanilla’ mock當(dāng)調(diào)用一個沒有stub的方法的時候會拋出一個異常。這會得到一個單調(diào)的mock,且在mock的生命周期中每一個方法調(diào)用都要被stub掉。(更多信息請看下一節(jié)關(guān)于stub)
如果你不想stub很多方法,用‘nice’ mock。‘nice’ mock非常有禮貌而且不會在一個沒有stub掉的方法被調(diào)用的時候拋出異常。
- id niceMockThing = [OCMock niceMockForClass[Thing class]];
OCMock3 新版本對應(yīng)API
- id mockThing = OCMClassMock([Thing class]);
最后一個mock類型是‘partial’ mock。當(dāng)一個沒有stub掉的方法被調(diào)用了,這個方法會被轉(zhuǎn)發(fā)到真實(shí)的對象上。這是對mock技術(shù)上的欺騙,但是非常有用,當(dāng)有一些類不適合讓自己很好的被stub。
- Thing *someThing = [Thing alloc] init];
- id aMock = [OCMockObject partialMockForObject:someThing]
OCMock3 新版本對應(yīng)API
- Thing *someThing = [Thing alloc] init];
- id aMock = OCMPartialMock(someThing);
驗(yàn)證方法是否被調(diào)用
驗(yàn)證方法是否被調(diào)用非常簡單。這個可以用expect來完成拒絕和驗(yàn)證方法:
- id niceMockThing = [OCMock niceMockForClass[Thing class]];
- [[niceMockThing expect] greeting:@"hello"];
- // verify the method was called as expected
- [niceMocking verify];
OCMock3 新版本對應(yīng)API
- id niceMockThing = OCMClassMock([Thing class]);
- OCMVerify([niceMockThing greeting:@"hello"]);
當(dāng)被驗(yàn)證的方法沒有被調(diào)用的時候會拋出異常。如果你用的是XCTest,那么請用XCTAssertNotThrow來包裝驗(yàn)證調(diào)用。拒絕方法調(diào)用也是同樣的道理,但是會再方法調(diào)用的時候拋出異常。就像stub,selector和傳遞過去驗(yàn)證的參數(shù)必須匹配調(diào)用時候傳遞過去的參數(shù)。用[OCMArg any]可以簡化我們的工作。
處理block參數(shù)
OCMock也可以處理block回調(diào)參數(shù)。block回調(diào)通常用于網(wǎng)絡(luò)代碼,數(shù)據(jù)庫代碼,或者在任何異步操作中。在這個例子中,思考下下面的方法:
- - (void)downloadWeatherDataForZip:(NSString *)zip
- callback:(void (^)(NSDictionary *response))callback;
在這個例子中,我們有一個下載天氣壓縮數(shù)據(jù)的方法,并且把下載下來的dictionary代理到一個block的回調(diào)中。在測試中,我們通過預(yù)定義的天氣數(shù)據(jù)來測試回調(diào)處理。這也是明智的測試失敗場景。你永遠(yuǎn)不會知道網(wǎng)絡(luò)上會返回你什么東西!
- // 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 新版本對應(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.這個mock對象使用帶NSInvocation參數(shù)的“andDo”方法。一個NSInvocation對象代表一個‘objectivetified’(實(shí)在不知道這個什么鬼)表現(xiàn)的方法調(diào)用。通過這個NSinvocation對象,使得攔截傳遞給我們的方法的block參數(shù)變得可能。
2.用與我們測試的方法中相同的方法簽名聲明一個block參數(shù)。
3.NSInvocation實(shí)例方法"getArgument:atIndex:"將賦值后的塊函數(shù)傳遞都原始函數(shù)中定義的塊函數(shù)中。注意:在Objective-C中,傳遞給任意方法的前兩個參數(shù)都是“self”和“_cmd”.這是一個運(yùn)行時的小功能以及用下標(biāo)來獲取NSInvocation參數(shù)時我們需要考慮的東西。
4.最后,傳遞這個回調(diào)的預(yù)定義字典。
最后
希望這篇文章和例子已經(jīng)陳述清楚一些OCMock最通用的用法。OCMock站點(diǎn):http://ocmock.org/features/是一個最好的學(xué)習(xí)OCMock的地方。mock是單調(diào)的但是對于一個現(xiàn)代的OO系統(tǒng)卻是必須的。如果一個依賴圖很難用mock來測試,這個跡象表明你的設(shè)計需要重新考慮了。