上篇《iOS開發 | 如何為網絡接口編寫單元測試》發表后,收到不少小伙伴的簡信,提出了不少問題,其中一個典型問題是:為已有的項目添加單元測試時,不知道如何入手。
為了回答這個問題,本篇將針對一個實例,演示測試網絡接口的實戰技巧。
我們來看一下,AF源碼中的Post類有個典型的網絡請求:
+ (NSURLSessionDataTask *)globalTimelinePostsWithBlock:(void (^)(NSArray *posts, NSError *error))block {
return [[AFAppDotNetAPIClient sharedClient] GET:@"stream/0/posts/stream/global"
parameters:nil
progress:nil
success:^(NSURLSessionDataTask * __unused task, id JSON) {
NSArray *postsFromResponse = [JSON valueForKeyPath:@"data"];
NSMutableArray *mutablePosts = [NSMutableArray arrayWithCapacity:[postsFromResponse count]];
for (NSDictionary *attributes in postsFromResponse) {
Post *post = [[Post alloc] initWithAttributes:attributes];
[mutablePosts addObject:post];
}
if (block) {
block([NSArray arrayWithArray:mutablePosts], nil);
}
} failure:^(NSURLSessionDataTask *__unused task, NSError *error) {
if (block) {
block([NSArray array], error);
}
}];
}
AFAppDotNetAPIClient繼承自AFHTTPSessionManager,調用GET方法,success回調處理了網絡返回的字典數據JSON,并將“data”中的數據解析成Post對象,存入數組,然后通過block返回給調用者,確實是非常典型的網絡數據處理。
我們想測試這個方法,
首要目標是確定在網絡獲得正常的數據時,能正確返回Post數組。
第一步:準備測試數據
1. 服務器返回的json數據
由于這里沒有一般項目中的《接口說明手冊》等開發文檔,我們通過瀏覽器訪問實際網絡
https://api.app.net/stream/0/posts/stream/global
,獲得服務器返回的實際json數據,做為我們的標準測試數據,這么做只是為了獲得一個實際數據的樣板,并不意味著我們的測試需要依賴網絡,后續可以按自己的需要編輯多個本地json文件,做為測試數據;
保存的json數據如下(文件名data.json,這里只展示部分截圖):
2.將 json數據轉成字典
這里使用YYKit提供的NSData+YYAdd 擴展中的 dataNamed()方法,它可以將文件中的json讀取為NSData對象:
NSData *jsonData = [NSData dataNamed:@"data.json"];
NSError *err;
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:jsonData
options:NSJSONReadingMutableContainers
error:&err];
第二步:調用目標方法
現在我們有了測試數據dic,接著需要將dic傳給success block,
觀察globalTimelinePostsWithBlock()的實現,我們發現這是一個類方法,如果我們mock一個Post對象,替換掉其block,這么做就繞過了globalTimelinePostsWithBlock的內部實現,顯然一點意義都沒有,因為主要邏輯都在AFAppDotNetAPIClient的success回調里;
而AFAppDotNetAPIClient是個單例,其實例及方法調用被封裝在方法中,無法從外部傳入mock對象,于是,我們的挑戰變成了找一個mock單例對象的方法,是否有這樣的方法呢?
使用OCMClassMock偽造單例對象
我們還是求助于OCMock來幫忙:OCMock3很貼心的加入了對單例對象的支持:
id classMock = OCMClassMock([AFAppDotNetAPIClient class]);
OCMStub([classMock sharedClient]).andReturn(mockManager);
[Post globalTimelinePostsWithBlock:^(NSArray *posts, NSError *error) {
NSLog(@"~~~");
}];
我們來解釋下代碼中OCMStub的作用:替換AFAppDotNetAPIClient的shareClient方法,在其被調用時,返回andReturn中指定的對象mockManager。
mockManager就是我們傳入測試數據的機會,完整測試用例如下:
- (void)testExample {
id mockManager = [OCMockObject mockForClass:[AFAppDotNetAPIClient class]];
[[[mockManager expect] andDo:^(NSInvocation *invocation) {
void (^successBlock)(NSURLSessionDataTask *task, id responseObject) = nil;
[invocation getArgument:&successBlock atIndex:5];
NSData *jsonData = [NSData dataNamed:@"data.json"];
NSError *err;
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:jsonData
options:NSJSONReadingMutableContainers
error:&err];
successBlock([[NSURLSessionDataTask alloc] init],
dic
);
}] GET:[OCMArg any]
parameters:nil
progress:[OCMArg any]
success:[OCMArg any]
failure:[OCMArg any]];
id classMock = OCMClassMock([AFAppDotNetAPIClient class]);
OCMStub([classMock sharedClient]).andReturn(mockManager);
[Post globalTimelinePostsWithBlock:^(NSArray *posts, NSError *error) {
XCTAssert(posts.count == 20, @"應該返回20個Post對象");
XCTAssertTrue([posts[0] isKindOfClass:[Post class]]);
}];
[classMock stopMocking];
}
注意測試結束后,使用[classMock stopMocking];恢復單例的狀態,以免影響其他測試用例。
這個例子中,我們選擇GET方法的success做為測試目標,沒有深入GET方法內部進行測試,因為我們相信AFNetworking已經做了足夠的測試,而我們的重點在于應用內部邏輯,
利用一系列技巧,我們既為方法的內部調用提供了測試數據,又沒有重寫目標方法的任何代碼。
這樣的“分界點”選擇,在測試實戰中是常見的挑戰。
本文展示了針對類方法,單例對象,從json轉成字典等單元測試常見問題的解決方案,希望能對網絡做測試的小伙伴提供一些啟發,歡迎來信,留言進一步交流。