如何对一个 NSURLConnection Delegate 进行单元测试?

11

如何对我的 NSURLConnection 代理进行单元测试? 我创建了一个 ConnectionDelegate 类,该类遵循不同的协议,以将来自 Web 的数据提供给不同的 ViewControllers。在进一步开始之前,我想先编写单元测试。 但是我不知道如何在没有网络连接的情况下对它们进行单元测试。我还想知道我应该如何处理异步回调。

6个回答

15
这与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];

由于测试对象未使用真实连接,因此我们现在可以从测试中调用委托方法。 对于NSURLResponse,我们正在使用另一个模拟,响应数据是从测试中的其他地方定义的字符串创建的。
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];

这就是了。你已经成功地模拟了测试对象与连接的所有交互,现在可以检查它是否处于应该处于的状态。
如果你想看一些“真实”的代码,请查看CCMenu项目中使用NSURLConnections的一个类的测试。这有点令人困惑,因为被测试的类也叫做connection。

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


这非常有用。我正在尝试将其扩展到涵盖来回对话而不是一次性请求/响应场景,但我没有太多的运气。我发布了一个单独的问题:http://stackoverflow.com/questions/16472878/how-can-you-use-ocmock-with-nsurlconnection-delegate-for-a-series-of-mock-networ - Gruntcakes

9

编辑(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 循环,测试已执行完成。完成。


5

在单元测试中,我避免使用网络。取而代之的是:

  • 我将 NSURLConnection 孤立在一个方法中。
  • 我创建了一个测试子类,重写该方法以删除所有 NSURLConnection 的痕迹。
  • 我编写一个测试来确保在需要时调用特定的方法。这样我就知道它在现实生活中会触发 NSURLConnection 了。

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


在我的委托方法中模拟或存根化 NSURLConnection 怎么样?我子类化了 NSURLConnection,以包含接收到的 NSData、接收器(我将要编写的 JSON 获取器)和用于调用它的请求。 - Moxy
在查看了我的代码并再次阅读了您的答案后,我发现我正在正确的轨道上! - Moxy
我的 NSURLConnection 已经被隔离在一个方法中 -(void)sendRequest:(NSURLRequest *) request forReceiver:(id <JSONDelegate>) receiver。我可以测试所有其他方法,以确保 URL 和请求格式正确。另一方面,通过重写 sendRequest 方法,我可以传递虚假数据来测试我的接收器。这样做有意义吗? - Moxy

5
我最喜欢的方法是子类化NSURLProtocol并使其响应所有http请求或其他协议。然后,在您的-setup方法中注册测试协议,并在您的-tearDown方法中取消注册。
您可以让此测试协议向您的代码返回一些众所周知的数据,以便您可以在单元测试中进行验证。
我已经写了几篇关于这个主题的博客文章。对于您的问题来说,最相关的可能是使用NSURLProtocol注入测试数据单元测试异步网络访问
您还可以查看我的ILCannedURLProtocol,它在先前的文章中有所描述。源代码可在Github上找到。

我读了这两篇文章,它们非常有趣。实际上,由于它们,我开始想到如何解决这个问题。我不认为我需要子类化NSURLProtocol(而且我不愿意这样做,以防将来发生变化),因为我的连接代理不处理数据。我正在使用我的接收器作为观察者。因此,通过覆盖启动连接的方法,我可以提供所需的数据。顺便说一句:很棒的博客!我已经将IL加入书签 ;) - Moxy
感谢您的评论。只是为了澄清,ILCannedURLProtocol并不打算进入生产代码,所以我不会太担心底层NSURLProtocol的更改。如果发生这种情况,那只是对您的测试设置进行更改。 - Claus Broch
MockNSURLConnection 非常适合您的方法,效果惊人。 - snez

0

尝试在您的帖子中描述链接所包含的内容。 - Dan Teesdale

-1

1
将响应限制为404、200和无响应。如果您想测试409、204、403和其他状态代码、ETags、压缩等,该怎么办?此外,如果您使用自动化构建服务器,您是否要为每个测试机安装Apache和这种东西? - Guillermo
1
这并不是一个真正应该被推荐的方法。Guillermo在他的评论中已经很好地解释了其中的一些原因。我认为我们应该在回答其他开发者时鼓励更好的编程实践... - Rob

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接