为网络接口编写单元测试(OCMock注入)

为网络接口编写单元测试(OCMock注入)_第1张图片

上篇《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,这里只展示部分截图):

为网络接口编写单元测试(OCMock注入)_第2张图片
服务器返回的原始数据

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

你可能感兴趣的:(为网络接口编写单元测试(OCMock注入))