上篇《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转成字典等单元测试常见问题的解决方案,希望能对网络做测试的小伙伴提供一些启发,欢迎来信,留言进一步交流。