用 NSURProtocol 注入测试数据
在之前的几篇博文中,笔者介绍过访问异步网络的单元测试方法及如何使用模拟对象来进一步控制单元测试的范围。在今天的教程中,笔者将展示另一种方法,即:通过自定义 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 注入测试数据的更多相关文章
- ABP中单元测试的技巧:Mock和数据驱动
		
(此文章同时发表在本人微信公众号"dotNET每日精华文章",欢迎右边二维码来关注.) 题记:虽然ABP为大家提供了测试的脚手架了,不过有些小技巧还是需要自己探索的. ASP.NE ...
 - Flink Program Guide (2) -- 综述 (DataStream API编程指导 -- For Java)
		
v\:* {behavior:url(#default#VML);} o\:* {behavior:url(#default#VML);} w\:* {behavior:url(#default#VM ...
 - 演练5-3:Contoso大学校园管理系统3
		
在前面的教程中,我们使用了一个简单的数据模型,包括三个数据实体.在这个教程汇中,我们将添加更多的实体和关系,按照特定的格式和验证规则等自定义数据模型. Contoso大学校园管理系统的数据模型如下. ...
 - spring面试问题与答案集锦
		
我收集了一些spring面试的问题,这些问题可能会在下一次技术面试中遇到.对于其他spring模块,我将单独分享面试问题和答案. 如果你能将在以前面试中碰到的,且你认为这些应该是一个有spring经验 ...
 - 【Python】UI自动化-1
		
一.安装selenium和环境配置 1 pip install selenium 2 三个驱动文件放到d:盘根目录 3 安装火狐版本33 4 安装插件:selenium ide\firebug\fir ...
 - Elasticsearch 系列4 --- Windows10安装Kibana
		
Kibana是Elastic Stack家族内的一部分,它是一个管理网站,与ES(Elastic Search)集成可以用来管理ES的索引,除ES外它还可以跟Elastic家族的其他组件进行整合如lo ...
 - Flink官网文档翻译
		
http://ifeve.com/flink-quick-start/ http://vinoyang.com/2016/05/02/flink-concepts/ http://wuchong.me ...
 - Spring手册
		
一.Spring 简介 二.结构体系 三.七大主要模块 四.Spring Maven依赖 五 .Sprinf framework 一.Spring 简介 spring是一个开源的轻量级的应用开发框架, ...
 - Spring入门学习笔记(1)
		
目录 Spring好处 依赖注入 面向面编程(AOP) Spring Framework Core Container Web Miscellaneous 编写第一个程序 IoC容器 Spring B ...
 
随机推荐
- C++学习(五)
			
一.拷贝构造函数和拷贝赋值运算符1.拷贝构造:用一个已有的对象,构造和它同类型的副本对象——克隆.2.形如class X { X (const X& that) { ... }};的构造函数 ...
 - java Spring 在WEB应用中的实例化
			
.前面讲解的都是通过直接读取配置文件,进行的实例化ApplicationContext AbstractApplicationContext app = new ClassPathXmlApplica ...
 - SQLServer获取最后插入的ID值SCOPE_IDENTITY、IDENT_CURRENT 和 @@IDENTITY的比较
			
IDENT_CURRENT 返回为任何会话和任何作用域中的特定表最后生成的标识值.IDENT_CURRENT 不受作用域和会话的限制,而受限于指定的表. @@IDENTITY 返回为当前会话的所有作用 ...
 - 转:四种方案解决ScrollView嵌套ListView问题
			
转载自:http://blog.sina.com.cn/s/blog_46798aa80101lxbk.html 原始的连接已经不知道是哪里了,项目中遇到了同样的问题,花了一下午都没有想到是嵌套引起的 ...
 - c#调用c++ dll(一)
			
首先来说说c++中的dll 核心的一些知识 比较大的应用程序都由很多模块组成,这些模块分别完成相对独立的功能,它们彼此协作来完成整个软件系统的工作.可能存在一些模块的功能较为通用,在构造其它软件系统时 ...
 - 获取API返回值
			
//$return=getApiResult($url); // if ($return==200){ // //..... // } function getApiResult($url){ if( ...
 - ios 消息跳转处理
			
一.消息转发流程 当向Objective-C对象发送一个消息,但runtime在当前类及父类中找不到此selector对应的方法时,消息转发(message forwarding)流程开始启动. 动态 ...
 - 游标、type使用示例
			
declare my_cur sys_refcursor; --定义游标变量 type v_record is record( --定义 record类型 obj_id number, ...
 - Struts2的输入校验
			
一.Struts2提供了基于验证框架的输入校验,在这种校验方式下,所有的输入校验只需要编写简单的配置文件,Struts2的验证框架将会负责进行服务器校验和客户端校验. 校验失败后将Struts2将自动 ...
 - 如何在Angular2中使用Forms
			
在Angular2中形成两个基本对象是Control和ControlGroup. 控制用户输入 Control 一个控制表示一个输入字段(ngFormControl) 一个控制可以绑定到一个input ...