該文章使用的API是OCMock老版本的API,新版本也相容老版本的API,譯者在用到老版本的API處已經新增了對應的新版本(OCMock3)的API供讀者參考。
愛好者
這篇文章假設讀者都能熟悉使用Xcode5的測試框架XCTest,或者BBD測試工具Kiwi或其他的iOS測試框架
什麼是mock?差不多就是紙老虎
當我們寫單元測試的時候,不可避免的要去儘可能少的例項化一些具體的元件來保持測試既短又快。而且保持單元的隔離。在現代的物件導向系統中,測試的元件很可能會有幾個依賴的物件。我們用mock來替代例項化具體的依賴class。mock是在測試中的一個偽造的有預定義行為的具體物件的替身物件。被測試的元件不知道其中的差異!你的元件是在一個更大的系統中被設計的,你可以很有信心的用mock來測試你的元件。
常見的mock使用案例
stub方法
我們用一個簡單的例子來開始解釋OCMock中一般的stub語法。
1 2 3 |
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 新版本對應API
1 2 3 |
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語法可能看起來有點奇怪,但這是普遍的做法:
ourMockObject stub] whatItShouldReturn ] method:
OCMock3 新版本對應API
OCMStub([ourMockObject method:]).andReturn()
一個非常重要的說明:注意[OCMArg any]
的用法。當指定一個帶引數的方法時,方法被呼叫並且引數為指定引數的話,mock會返回andReturn:指定的值。[OCMArg any]
方法告訴stub匹配所有的引數值。舉個例子:
[car goFaster:84 units:@"mph"];
不會觸發stub,因為最後一個引數不匹配”kph”.
類方法
OCMock會在mock例項上沒有找到相同名字的例項方法的時候去找同名的類方法。在名字相同的情況下(類方法和例項方法同名),用classMethod
來指定類方法:
[[[[jalopy stub] classMethod] andReturn:@"expired"] checkWarrany];
在OCMock3中classMethod和instanceMethod的stub方式一樣,例如:
1 2 3 4 |
id classMock = OCMClassMock([SomeClass class]); OCMStub([classMock aClassMethod]).andReturn(@"Test string"); // result is @"Test string" NSString *result = [SomeClass aClassMethod]; |
mock型別 – niceMock,partialMock
OCMock提供了幾種不同型別的mock,每個都有他們特定的使用場景。
用這種方式來建立任意mock:
id mockThing = [OCMock mockForClass[Thing class]];
OCMock3 新版本對應API
id mockThing = OCMStrictClassMock([Thing class]);
這就是我所說的‘vanilla’ mock
。‘vanilla’ mock
當呼叫一個沒有stub的方法的時候會丟擲一個異常。這會得到一個單調的mock,且在mock的生命週期中每一個方法呼叫都要被stub掉。(更多資訊請看下一節關於stub)
如果你不想stub很多方法,用‘nice’ mock
。‘nice’ mock
非常有禮貌而且不會在一個沒有stub掉的方法被呼叫的時候丟擲異常。
id niceMockThing = [OCMock niceMockForClass[Thing class]];
OCMock3 新版本對應API
id mockThing = OCMClassMock([Thing class]);
最後一個mock型別是‘partial’ mock
。當一個沒有stub掉的方法被呼叫了,這個方法會被轉發到真實的物件上。這是對mock技術上的欺騙,但是非常有用,當有一些類不適合讓自己很好的被stub。
1 2 |
Thing *someThing = [Thing alloc] init]; id aMock = [OCMockObject partialMockForObject:someThing] |
OCMock3 新版本對應API
1 2 |
Thing *someThing = [Thing alloc] init]; id aMock = OCMPartialMock(someThing); |
驗證方法是否被呼叫
驗證方法是否被呼叫非常簡單。這個可以用expect
來完成拒絕和驗證方法:
1 2 3 4 |
id niceMockThing = [OCMock niceMockForClass[Thing class]]; [[niceMockThing expect] greeting:@"hello"]; // verify the method was called as expected [niceMocking verify]; |
OCMock3 新版本對應API
1 2 |
id niceMockThing = OCMClassMock([Thing class]); OCMVerify([niceMockThing greeting:@"hello"]); |
當被驗證的方法沒有被呼叫的時候會丟擲異常。如果你用的是XCTest,那麼請用XCTAssertNotThrow
來包裝驗證呼叫。拒絕方法呼叫也是同樣的道理,但是會再方法呼叫的時候丟擲異常。就像stub,selector和傳遞過去驗證的引數必須匹配呼叫時候傳遞過去的引數。用[OCMArg any]
可以簡化我們的工作。
處理block引數
OCMock也可以處理block回撥引數。block回撥通常用於網路程式碼,資料庫程式碼,或者在任何非同步操作中。在這個例子中,思考下下面的方法:
1 2 |
- (void)downloadWeatherDataForZip:(NSString *)zip callback:(void (^)(NSDictionary *response))callback; |
在這個例子中,我們有一個下載天氣壓縮資料的方法,並且把下載下來的dictionary代理到一個block的回撥中。在測試中,我們通過預定義的天氣資料來測試回撥處理。這也是明智的測試失敗場景。你永遠不會知道網路上會返回你什麼東西!
1 2 3 4 5 6 7 8 9 10 |
// 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 新版本對應API
1 2 3 4 5 6 7 8 9 10 |
// 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); }); |
這裡的大體思想相當簡單,即便如此,他的實現也需要一些說明:
1 2 3 4 |
1.這個mock物件使用帶NSInvocation引數的“andDo”方法。一個NSInvocation物件代表一個‘objectivetified’(實在不知道這個什麼鬼)表現的方法呼叫。通過這個NSinvocation物件,使得攔截傳遞給我們的方法的block引數變得可能。 2.用與我們測試的方法中相同的方法簽名宣告一個block引數。 3.NSInvocation例項方法"getArgument:atIndex:"將賦值後的塊函式傳遞都原始函式中定義的塊函式中。注意:在Objective-C中,傳遞給任意方法的前兩個引數都是“self”和“_cmd”.這是一個執行時的小功能以及用下標來獲取NSInvocation引數時我們需要考慮的東西。 4.最後,傳遞這個回撥的預定義字典。 |
最後
希望這篇文章和例子已經陳述清楚一些OCMock最通用的用法。OCMock站點:http://ocmock.org/features/是一個最好的學習OCMock的地方。mock是單調的但是對於一個現代的OO系統卻是必須的。如果一個依賴圖很難用mock來測試,這個跡象表明你的設計需要重新考慮了。