在之前的几篇博文中,笔者介绍过访问异步网络的单元测试方法及如何使用模拟对象来进一步控制单元测试的范围。在今天的教程中,笔者将展示另一种方法,即:通过自定义 NSURProtocol 类来获取静态测试数据,从而为测试提供可靠的数据。

几个月前,Gowalla 在 GitHub 上公开了他们用于 iPhone 客户端的网络代码。这个被称为 AFNetworking 的库,是一个「使用 NSOperations 和 block 回调的、讨喜的 iOS 网络库」。这段代码中首先吸引笔者的一点,是利用该库内置的支持服务,仅需几行代码即可访问基于 JSON 的服务。

AFNetworking 的界面之简洁,启发笔者运行一次快速的测试,并编写ILBitly。ILBitly 可提供一个基于 Objective C 的包装类,从而获得 Bitly 的 URL 缩短服务。AFNetworking 的使用非常简单,尤其是 JSON 的支持服务,仅需调用单个类的方法即可获得。然而,这简洁性也为我们使用 MCMock 编写自包含单元和模拟测试增添了不少难度。这主要是因为 OCMock 不支持类方法的模拟。笔者也尝试过其它方法,例如 method swizzling,然而并没有成功。

就在几天前,笔者看到 GitHub 上的一则讨论,有关如何恰当地模拟 AFNetworking 的接口。讨论中 Adam Ernst 建议使用自定义的 NSURLProtocol 来完成这项任务。这让笔者灵光一现,终于想到了解决测试问题的方法。

子类化 NSURLProtocol

如上文所述,笔者需要拦截网络访问,但当时找不到一种简单的方法来模拟 AFJSONRequestOperation 的接口。于是想到了另一条路,即拦截 iOS 内置的标准 http 协议。这可以通过注册自定义的NSURLProtocol 子类 ILCannedURLProtocol 来实现。该子类可处理 http 请求。由于询问协议处理器的顺序与注册顺序是相反的。因此相较于标准类,我们的类总是会被优先访问。

这样做的主要目的,是每当出现一个 http 请求,ILCannedURLProtocol 即会回应一组预先加载好的测试数据。如此一来,我们就能在测试中消除所有外部影响。同时,可以在需要时,故意使 http 请求失败。ILCannedURLProtocol 的接口如下所示:

@interface ILCannedURLProtocol : NSURLProtocol
+ (void)setCannedResponseData:(NSData*)data;
+ (void)setCannedHeaders:(NSDictionary*)headers;
+ (void)setCannedStatusCode:(NSInteger)statusCode;
+ (void)setCannedError:(NSError*)error;
@end

在现有 http 请求的形式下,我们不能替换任何一个请求的全部内容。举例来说,我们只能拦截 GET 请求,却无法拦截任何类型的权限认证质询(authentication challenge)或认证应答(authentication response)。但它现有的功能已经足以为测试 ILBitly 及其它相似的类提供测试数据。

基本上每个 setCannedXxx 方法都会保留传给它的对象,因此每当http 请求需要时,可以返回这些对象。但这也意味着它们只能每次应对一组测试数据。

子类化 NSURLProtocol 还需要实现一些其他的方法。其中之一是canInitWithRequest:每当发起一个 NSURLRequest 时,都会调用该方法,来判断该类是否支持这一请求。我们将使用这个方法来拦截 http GET 请求:

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
// For now only supporting http GET
return [[[request URL] scheme] isEqualToString:@"http"]
&& [[request HTTPMethod] isEqualToString:@"GET"];
}

同时我们也需要实现 startLoading 方法。该方法会在每次实例化相关协议处理器时被调用,从而给请求提供数据。根据设置的封装数据不同,我们的方法将会给出一个成功的回应,或者报出一个错误:

- (void)startLoading {
NSURLRequest *request = [self request];
id client = [self client]; if(gILCannedResponseData) {
// Send the canned data
NSHTTPURLResponse *response =
[[NSHTTPURLResponse alloc] initWithURL:[request URL]
statusCode:gILCannedStatusCode
headerFields:gILCannedHeaders
requestTime:0.0]; [client URLProtocol:self didReceiveResponse:response
cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[client URLProtocol:self didLoadData:gILCannedResponseData];
[client URLProtocolDidFinishLoading:self]; [response release];
}
else if(gILCannedError) {
// Send the canned error
[client URLProtocol:self didFailWithError:gILCannedError];
}
}

如果你决定在自己的项目中使用上述代码测试,小心不要把它写入任何打算上传到 APP Store 的产品代码中去。如果你不明白为什么,让我们来看一下 NSHTTPURLResponse 的初始化程序。这是一个私有 API,通过在 iOS 4.3 SDK 上运行 class-dump 来获取。如果你把这段回调加在产品代码中,苹果可能会拒绝它。苹果甚至可能会在未来的 iOS更新中对它进行修改,尽管可能性不大。 但如果只是用它来跑单元测试的话,那应该没什么问题。

除去另外几个基本为空的方法,所有的方法都在这了。现在只需注册我们自定义的类,然后再加载一些封装数据进去。

准备单元测试

The unit test class for ILBitly just includes a few instance variables:

@interface ILBitlyTest : SenTestCase {
ILBitly *bitly;
id bitlyMock;
BOOL done;
}
@end

变量 bitly 包含 test下ILBitly 代码的一个实例,bitlyMock 包含了用作 ILBitly 测试的部分 mock 对象,done 是异步调用结束的信号。后面笔者会详细地解释这些变量。

执行每个测试用例之前,setUp 方法都会被自动调用,来做以下准备:

- (void)setUp
{
[super setUp]; // Init bitly proxy using test id and key - not valid for real use
bitly = [[ILBitly alloc] initWithLogin:@"LOGIN" apiKey:@"KEY"];
done = NO; [NSURLProtocol registerClass:[ILCannedURLProtocol class]];
[ILCannedURLProtocol setCannedStatusCode:200];
}

我们这个方法来准备默认的测试实例,以及注册ILCannedURLProtocol。那些用来实例化 ILBitly 的参数只是传给服务请求的占位符。因为之后我们会使用静态测试数据,所以它们其实并没有什么实际用途,仅供稍后确认它们是否被如期传递。

为了平衡资源,每次测试后,我们都会注销自定义协议,同时销毁测试数据。

- (void)tearDown
{
[NSURLProtocol unregisterClass:[ILCannedURLProtocol class]];
[ILCannedURLProtocol setCannedHeaders:nil];
[ILCannedURLProtocol setCannedResponseData:nil];
[ILCannedURLProtocol setCannedError:nil]; [bitly release];
bitlyMock = nil; [super tearDown];
}

我们也需要准备一些测试数据。这很容易:如上一篇博文所说,我们可以用 curl 来保存从 bitly 到 JSON 文件的原始应答,然后在每个测试用例中加载出来。

动手组装

最后,我们写些测试来验证 ILBitly 代码。例如,下文是一个验证缩短 URL 服务的测试:

- (void)testShorten {
// Prepare the canned test result
[ILCannedURLProtocol setCannedResponseData:[self cannedDataWithName:@"shorten"]];
[ILCannedURLProtocol setCannedHeaders:
[NSDictionary dictionaryWithObject:@"application/json; charset=utf-8"
forKey:@"Content-Type"]]; // Prepare the mock
bitlyMock = [OCMockObject partialMockForObject:bitly];
NSURL *trigger = [NSURL URLWithString:@"http://"];
[[[bitlyMock expect] andReturn:[NSURLRequest requestWithURL:trigger]]
requestForURLString:[OCMArg checkWithBlock:^(id url) {
return [url isEqualToString:EXPECTED_REQUEST];
}]]; // Execute the code under test
[bitly shorten:@"http://www.infinite-loop.dk/blog/" result:^(NSString *result) {
STAssertEqualObjects(result, @"http://j.mp/qA7S4Q", @"Unexpected short url");
done = YES;
} error:^(NSError *err) {
STFail(@"Shorten failed with error: %@", [err localizedDescription]);
done = YES;
}]; // Verify the result
STAssertTrue([self waitForCompletion:5.0], @"Timeout");
[bitlyMock verify];
}

在第一部分中,静态测试数据被加载到测试协议中。

之后我们为 bitly 对象创建了部分模拟对象。它的主要功能是拦截对requestForURLString 的内部调用,并创建一个我们期望调用的 URL。调用时,测试会验证是否向我们期望的URL发出了请求,并最终返回一个 NSURLRequest 实例。为触发加载我们自定义的协议,该实例只包含了基本的 URL Scheme。

被测试的代码可如第三部分所示被执行。由于调用(invoke) shorten:result:error后,block 随时可能被回调,我们设置了done,这样一来调用时我们就能知道了。

如上一篇博文所述,最后的一段代码将会给 done 信号最多 5 秒的等待时间。最后,确认模拟对象被调回,从而确认已经收到了所期望的信息。

如果我们转而想测试系统对错误的处理,我们只需替换掉测试方法的第一部分,改为错误数据,同时相应地对测试做如下改动:

  [ILCannedURLProtocol setCannedError:
[NSError errorWithDomain:NSURLErrorDomain
code:kCFURLErrorTimedOut
userInfo:nil]];

结论

综上所述,我们可以利用 NSURLProtocol 将可预测的测试数据注入单元测试和模拟测试中,以减少外部因素的影响。我们甚至可以扩展这些测试。举例来说,你可以用这个方法模拟糟糕的网络环境,如长延迟和窄带宽。可能性是无穷的,笔者仅希望可用此文抛砖引玉。

本文中所使用的 ILBitly 包及测试类都可在 GitHub 上找到,同时笔者还放了一个 iPhone APP 样例,用以演示某些功能。

更新:ILCannedURLProtocol 类也已放到 Github的 ILTesting 库中。

针对现在的信息就是做的处理。

欢迎各类评论与建议。原文地址:http://www.infinite-loop.dk/blog/2011/09/using-nsurlprotocol-for-injecting-test-data/

OneAPM Mobile Insight,监控网络请求及网络错误,提升用户留存。访问 OneAPM 官方网站感受更多应用性能优化体验,想阅读更多技术文章,请访问 OneAPM 官方技术博客

本文转自 OneAPM 官方博客

用 NSURProtocol 注入测试数据的更多相关文章

  1. ABP中单元测试的技巧:Mock和数据驱动

    (此文章同时发表在本人微信公众号"dotNET每日精华文章",欢迎右边二维码来关注.) 题记:虽然ABP为大家提供了测试的脚手架了,不过有些小技巧还是需要自己探索的. ASP.NE ...

  2. Flink Program Guide (2) -- 综述 (DataStream API编程指导 -- For Java)

    v\:* {behavior:url(#default#VML);} o\:* {behavior:url(#default#VML);} w\:* {behavior:url(#default#VM ...

  3. 演练5-3:Contoso大学校园管理系统3

    在前面的教程中,我们使用了一个简单的数据模型,包括三个数据实体.在这个教程汇中,我们将添加更多的实体和关系,按照特定的格式和验证规则等自定义数据模型. Contoso大学校园管理系统的数据模型如下. ...

  4. spring面试问题与答案集锦

    我收集了一些spring面试的问题,这些问题可能会在下一次技术面试中遇到.对于其他spring模块,我将单独分享面试问题和答案. 如果你能将在以前面试中碰到的,且你认为这些应该是一个有spring经验 ...

  5. 【Python】UI自动化-1

    一.安装selenium和环境配置 1 pip install selenium 2 三个驱动文件放到d:盘根目录 3 安装火狐版本33 4 安装插件:selenium ide\firebug\fir ...

  6. Elasticsearch 系列4 --- Windows10安装Kibana

    Kibana是Elastic Stack家族内的一部分,它是一个管理网站,与ES(Elastic Search)集成可以用来管理ES的索引,除ES外它还可以跟Elastic家族的其他组件进行整合如lo ...

  7. Flink官网文档翻译

    http://ifeve.com/flink-quick-start/ http://vinoyang.com/2016/05/02/flink-concepts/ http://wuchong.me ...

  8. Spring手册

    一.Spring 简介 二.结构体系 三.七大主要模块 四.Spring Maven依赖 五 .Sprinf framework 一.Spring 简介 spring是一个开源的轻量级的应用开发框架, ...

  9. Spring入门学习笔记(1)

    目录 Spring好处 依赖注入 面向面编程(AOP) Spring Framework Core Container Web Miscellaneous 编写第一个程序 IoC容器 Spring B ...

随机推荐

  1. mysql 中间件 分析

    360的Atlas 1.读写分离 2.从库负载均衡 3.IP过滤 4.自动分表 5.DBA可平滑上下线DB 6.自动摘除宕机的DB altas 在10000/s的请求量级应该是毫无问题的 https: ...

  2. Nginx高性能服务器安装、配置、运维 (6) —— Nginx日志及日志分割

    七.Nginx日志及日志分割 (1)Nginx日志文件 查看Nginx配置文件: 找到access_log,yum安装默认存储在/var/log/nginx目录下,且默认main格式: main格式定 ...

  3. 史上最全的JavaScript工作笔记

    /* * JavaScript查看对象函数 */ function resultTest( obj ){ var resultTest = ''; $.each(obj,function(key,va ...

  4. 安卓百度地图开发so文件引用失败问题研究

    博客: 安卓之家 微博: 追风917 CSDN: 蒋朋的家 简书: 追风917 博客园: 追风917 # 问题 首先,下面的问题基本都是在Android Studio下使用不当导致,eclipse是百 ...

  5. Android开发之ViewPager

    什么是ViewPager? ViewPager是安卓3.0之后提供的新特性,继承自ViewGroup,专门用以实现左右滑动切换View的效果. 如果想向下兼容就必须要android-support-v ...

  6. MVC中实现部分内容异步加载

    MVC中实现部分内容异步加载 action中定义一个得到结果集的方法 public ActionResult GetItemTree(string title, int itemid, int? pa ...

  7. Angularjs总结(八)$ cookie和$rootscope

    AngularJS 提供了很好的 $cookie 和 $cookieStore API 用来处理 cookies .这两个服务都能够很好的发挥HTML5 cookies,当HTML5 API可用时浏览 ...

  8. JAVA学习-JAVA环境准备

    dir:列出当前目录下的文件以及文件夹md:  创建目录rd:  删除目录cd: 进入指定的目录,打开文件夹cd..:退回到上一级目录cd/或cd\:退回到根目录del:删除文件d: : 切换到D盘根 ...

  9. C++ 引用(&)

    #include <iostream> void sort(int &a, int &b){ if (a>=b) { return; } if (a<b) { ...

  10. Greedy is Good

    作者:supernova 出处:http://community.topcoder.com/tc?module=Static&d1=tutorials&d2=greedyAlg Joh ...