如何单元测试一个NSURLConnection委托?

问题描述:

我该如何测试我的NSURLConnection委托? 我做了一个ConnectionDelegate类,它符合不同的协议,以将数据从Web提供给不同的ViewController。在我变得太远之前,我想开始编写我的单元测试。 但我不知道如何测试他们作为一个单位没有互联网连接。我也想我应该做什么来处理异步回调。如何单元测试一个NSURLConnection委托?

虽然这与Jon的回应类似,但无法将其纳入评论。第一步是确保你没有创建一个真正的连接。实现此目的的最简单方法是将连接的创建拉入工厂方法,然后在测试中替换工厂方法。有了OCMock的部分模拟支持,这可能看起来像这样。

在你真正的类:

- (NSURLConnection *)newAsynchronousRequest:(NSURLRequest *)request 
{ 
    return [[NSURLConnection alloc] initWithRequest:request delegate:self]; 
} 

在您的测试:

id objectUnderTest = /* create your object */ 
id partialMock = [OCMockObject partialMockForObject:objectUnderTest]; 
NSURLConnection *dummyUrlConnection = [[NSURLConnection alloc] 
    initWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"file:foo"]] 
    delegate:nil startImmediately:NO]; 
[[[partialMock stub] andReturn:dummyUrlConnection] newAsynchronousRequest:[OCMArg any]]; 

现在,当被测你的对象尝试创建它实际上得到测试所创建的虚拟连接的URL连接。虚拟连接不必是有效的,因为我们没有启动它,它永远不会被使用。如果你的代码使用连接,你可以返回另一个模拟,一个嘲笑NSURLConnection。

第二步是调用触发NSURLConnection的创建你的对象上的方法:由于被测对象没有使用真正的方面,我们现在可以调用从委托方法

[objectUnderTest doRequest]; 

测试。

int statusCode = 200; 
id responseMock = [OCMockObject mockForClass:[NSHTTPURLResponse class]]; 
[[[responseMock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode]; 
[objectUnderTest connection:dummyUrlConnection didReceiveResponse:responseMock]; 

NSData *responseData = [RESPONSE_TEXT dataUsingEncoding:NSASCIIStringEncoding]; 
[objectUnderTest connection:dummyUrlConnection didReceiveData:responseData]; 

[objectUnderTest connectionDidFinishLoading:dummyUrlConnection]; 

就是这样:对于我们使用另一种模拟的NSURLResponse,响应数据是从已定义的其他地方测试中的字符串创建。您已经有效地伪造了测试对象与连接之间的所有交互,现在您可以检查它是否处于应该处于的状态。

如果您想查看一些“真实”代码,查看使用NSURLConnections的CCMenu项目中的类的测试。这有点令人困惑,因为被测试的类也被命名为连接。

http://ccmenu.svn.sourceforge.net/viewvc/ccmenu/trunk/CCMenuTests/Classes/CCMConnectionTest.m?revision=129&view=markup

+0

这非常有用。我试图扩展它来覆盖来回的对话,而不是一次性的请求/响应场景,但我没有太多的运气。我发布了一个单独的问题:http://*.com/questions/16472878/how-can-you-use-ocmock-with-nsurlconnection-delegate-for-a-series-of-mock-networ – Gruntcakes 2013-05-09 23:34:19

这是我做的:

  1. 获取XAMPP控制面板 http://www.apachefriends.org/en/xampp.html
  2. 启动Apache服务器
  3. 在你~/Sites文件夹,把一个测试文件(任何你想要的数据,我们会打电话给它my file.test)。
  4. 使用URL启动您的代理http://localhost/~username/myfile.test
  5. 停止Apache服务器时,不使用它。
+1

这限制在404,200和无响应的响应。如果你想测试,冲突409,204,403和其他状态代码,etags,压缩等等...... 另外,如果你使用自动构建服务器,你打算安装apache和这个每种测试机器都有哪些东西? – Guillermo 2012-07-17 11:23:20

+1

不是一种真正应该作为推荐被传递的方法。吉列尔莫在他的评论中涵盖了一些很好的理由。我认为我们应该鼓励更好的编程实践,而不是开发人员的回答...... – Rob 2013-12-16 16:12:47

我在单元测试中避免了联网。相反:

  • 我在方法中分离NSURLConnection。
  • 我创建了一个测试子类,覆盖该方法以删除NSURLConnection的所有痕迹。
  • 我写了一个测试,以确保所需的方法在需要时被调用。然后我知道它会在现实生活中引发NSURLConnection。

然后我专注于更有趣的部分:合成具有各种特征的模拟NSURLResponses,并将它们传递给NSURLConnectionDelegate方法。

+0

在我的委托方法中嘲笑或存根NSURLConnection怎么样?我将NSURLConnection分类为包含接收到的NSData,接收者(我将写入的JSON获取者)以及用于调用它的请求。 – Moxy 2012-03-29 06:36:49

+0

看完我的代码并再次阅读您的答案后,我发现我正处在正确的轨道上! – Moxy 2012-03-29 07:35:28

+0

我的NSURLConnection已经被隔离在一个方法 - (void)sendRequest:(NSURLRequest *)请求forReceiver:(id )接收方。我可以测试所有其他方法以确保URL和请求格式正确。另一方面,通过重写sendRequest方法,我可以传递伪数据来测试我的接收器。这有意义吗? – Moxy 2012-03-29 07:44:14

我最喜欢的做法是将NSURLProtocol子类化,然后让它响应所有http请求 - 或者其他协议。然后,您在-setup方法中注册测试协议,并将其注销到-tearDown方法中。 然后,您可以使用此测试协议将一些众所周知的数据提供回您的代码,以便您可以在单元测试中验证它。

我写了一些关于这个问题的博客文章。与您的问题最相关的可能是Using NSURLProtocol for Injecting Test DataUnit Testing Asynchronous Network Access

您可能还想看看我的以前的文章中描述的ILCannedURLProtocol。来源可在Github

+0

我读了两篇文章,他们非常有趣。我实际上开始就如何解决这个问题感谢他们。我不认为我需要子类化NSURLProtocol(并且我更喜欢在未来会更改它的情况下不这样做),因为我的连接委托不处理数据。我正在使用我的接收器作为观察者。所以通过重写开始连接的方法,我可以提供我想要的数据。 PS:伟大的博客!我已加入IL;) – Moxy 2012-03-29 11:52:58

+0

感谢您的意见。只是为了澄清,ILCannedURLProtocol并不打算找到生产代码的方式,所以我不会太担心底层NSURLProtocol的变化。如果发生这种情况,那只是对您的测试设置的改变。 – 2012-03-29 11:58:38

+0

[MockNSURLConnection](https://github.com/wfleming/MockNSURLConnection)与您的方法非常吻合。 – snez 2012-12-20 14:22:21

EDIT(2014年2月18日):我只是碰到这种文章跌跌撞撞用更优雅的解决方案。

http://www.infinite-loop.dk/blog/2011/04/unittesting-asynchronous-network-access/

从本质上讲,你有以下方法:

- (BOOL)waitForCompletion:(NSTimeInterval)timeoutSecs { 
    NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeoutSecs]; 

    do { 
     [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:timeoutDate]; 
     if([timeoutDate timeIntervalSinceNow] < 0.0) 
      break; 
    } while (!done); 

    return done; 
} 

在您的测试方法的末尾,你要确保事情还没有超时:

STAssertTrue([self waitForCompletion:5.0], @"Timeout"); 

基本格式:

- (void)testAsync 
{ 
    // 1. Call method which executes something asynchronously 
    [obj doAsyncOnSuccess:^(id result) { 
     STAssertNotNil(result); 
     done = YES; 
    } 
    onError:^(NSError *error) [ 
     STFail(); 
     done = YES; 
    } 

    // 2. Determine timeout 
    STAssertTrue([self waitForCompletion:5.0], @"Timeout"); 
}  

==============

我迟到了,但我遇到了一个非常简单的解决方案。 (非常感谢http://www.cocoabuilder.com/archive/xcode/247124-asynchronous-unit-testing.html

h文件:

@property (nonatomic) BOOL isDone; 

.m文件:

- (void)testAsynchronousMethod 
{ 
    // 1. call method which executes something asynchronously. 

    // 2. let the run loop do its thing and wait until self.isDone == YES 
    self.isDone = NO; 
    NSDate *untilDate; 
    while (!self.isDone) 
    { 
     untilDate = [NSDate dateWithTimeIntervalSinceNow:1.0] 
     [[NSRunLoop currentRunLoop] runUntilDate:untilDate]; 
     NSLog(@"Polling..."); 
    } 

    // 3. test what you want to test 
} 

isDone在所述异步方法执行线程设置为YES

因此,在这种情况下,我在步骤1创建并启动NSURLConnection,并将其委托给此测试类。在

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response

我设置self.isDone = YES;。我们跳出while循环并执行测试。完成。

+0

太棒了。给我足够的东西,使东西工作:)谢谢。 – Johan 2013-12-24 13:36:53

+0

尝试描述您的文章中链接包含的内容。 – 2013-06-27 01:49:45