如何在iOS 7.1中使用NSURLSessiontestingHTTP请求和响应?

我想知道如何使用NSURLSession“unit testing”HTTP请求和响应。 现在,我的完成块代码在作为unit testing运行时不会被调用。 但是,当从AppDelegate (didFinishWithLaunchingOptions)中执行相同的代码时,将调用完成块中的代码。 正如在这个线程中所build议的, NSURLSessionDataTask dataTaskWithURL完成处理程序没有被调用 ,需要使用信号量和/或dispatch_group“确保主线程被阻塞,直到networking请求结束。

我的HTTP发布代码如下所示。

 @interface LoginPost : NSObject - (void) post; @end @implementation LoginPost - (void) post { NSURLSessionConfiguration* conf = [NSURLSessionConfiguration defaultSessionConfiguration]; NSURLSession* session = [NSURLSession sessionWithConfiguration:conf delegate:nil delegateQueue:[NSOperationQueue mainQueue]]; NSURL* url = [NSURL URLWithString:@"http://www.example.com/login"]; NSString* params = @"username=test@xyz.com&password=test"; NSMutableRequest* request = [NSMutableURLRequest requestWithURL:url]; [request setHTTPMethod:@"POST"]; [request setHTTPBody:[params dataUsingEncoding:NSUTF8StringEncoding]]; NSURLSessionDataTask* task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { NSLog(@"Response:%@ %@\n", response, error); //code here never gets called in unit tests, break point here never is triggered as well //response is actually deserialized to custom object, code omitted }]; [task resume]; } @end 

testing的方法如下所示。

 - (void) testPost { LoginPost* loginPost = [LoginPost alloc]; [loginPost post]; //XCTest continues by operating assertions on deserialized HTTP response //code omitted } 

在我的AppDelegate ,完成块代码确实工作,它看起来像下面。

 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { //generated code LoginPost* loginPost = [LoginPost alloc]; [loginPost post]; } 

在unit testing中运行时如何获得完成块的任何指针? 我是iOS的新手,所以一个明确的例子真的有帮助。

  • 注:我意识到我所要求的也许不是严格意义上的“单元”testing,因为我依赖于HTTP服务器作为testing的一部分(意思是我所要求的更像集成testing)。
  • 注:我知道有另一个线程unit testing与NSURLSession有关NSURLSession的unit testing,但我不想嘲笑响应。

Xcode 6现在使用XCTestExpectation处理asynchronoustesting。 当testing一个asynchronous进程时,你build立了这个进程asynchronous完成的“期望”,在发出asynchronous进程之后,你等待期望在一段固定的时间内得到满足,当查询结束时,你将asynchronous满足期望。

例如:

 - (void)testDataTask { XCTestExpectation *expectation = [self expectationWithDescription:@"asynchronous request"]; NSURL *url = [NSURL URLWithString:@"http://www.apple.com"]; NSURLSessionTask *task = [self.session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { XCTAssertNil(error, @"dataTaskWithURL error %@", error); if ([response isKindOfClass:[NSHTTPURLResponse class]]) { NSInteger statusCode = [(NSHTTPURLResponse *) response statusCode]; XCTAssertEqual(statusCode, 200, @"status code was not 200; was %d", statusCode); } XCTAssert(data, @"data nil"); // do additional tests on the contents of the `data` object here, if you want // when all done, Fulfill the expectation [expectation fulfill]; }]; [task resume]; [self waitForExpectationsWithTimeout:10.0 handler:nil]; } 

我以前的答案,在XCTestExpectation之前,但我会保留它的历史目的。


由于您的testing在主队列上运行,并且因为您的请求正在asynchronous运行,所以您的testing将不会捕获完成块中的事件。 您必须使用信号量或调度组来使请求同步。

例如:

 - (void)testDataTask { dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); NSURL *url = [NSURL URLWithString:@"http://www.apple.com"]; NSURLSessionTask *task = [self.session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { XCTAssertNil(error, @"dataTaskWithURL error %@", error); if ([response isKindOfClass:[NSHTTPURLResponse class]]) { NSInteger statusCode = [(NSHTTPURLResponse *) response statusCode]; XCTAssertEqual(statusCode, 200, @"status code was not 200; was %d", statusCode); } XCTAssert(data, @"data nil"); // do additional tests on the contents of the `data` object here, if you want // when all done, signal the semaphore dispatch_semaphore_signal(semaphore); }]; [task resume]; long rc = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 60.0 * NSEC_PER_SEC)); XCTAssertEqual(rc, 0, @"network request timed out"); } 

信号量将确保testing不会完成,直到请求。

很明显,我上面的testing只是做一个随机的HTTP请求,但希望它说明了这个想法。 而我的各种XCTAssert语句将识别四种types的错误:

  1. NSError对象不是零。

  2. HTTP状态码不是200。

  3. NSData对象是零。

  4. 完成块在60秒内没有完成。

你大概也会为响应的内容添加testing(在这个简单的例子中我没有这样做)。

请注意,上面的testing是有效的,因为我的完成块中没有任何东西是派发任何东西到主队列。 如果你用一个需要主队列的asynchronous操作来testing(如果你不小心使用AFNetworking或者自己手动派发到主队列中,会发生这种情况),你可以用上面的模式得到死锁(因为我们阻塞等待networking请求完成的主线程)。 但在NSURLSession的情况下,这种模式很好。


您询问了如何从命令行进行testing,与模拟器无关。 有几个方面:

  1. 如果你想从命令行testing,你可以在命令行中使用xcodebuild 。 例如,要从命令行在模拟器上进行testing,将会是(在我的示例中,我的scheme被称为NetworkTest ):

     xcodebuild test -scheme NetworkTest -destination'platform = iOS模拟器,name = iPhone Retina(3.5英寸),OS = 7.0'
    

    这将build立该计划,并运行在指定的目的地。 请注意,有很多关于Xcode 5.1问题的报告,从命令行使用xcodebuild在模拟器上testing应用程序(我可以validation这种行为,因为我有一台机器上面的工作正常,但冻结在另一个)。 在Xcode 5.1中模拟器的命令行testing似乎并不完全可靠。

  2. 如果你不想让你的testing在模拟器上运行(无论是从命令行还是从Xcode执行),那么你可以构build一个MacOS X目标,并为该构build提供相关的scheme。 例如,我添加了一个Mac OS X目标到我的应用程序,然后为它添加了一个名为NetworkTestMacOS的scheme。

    顺便说一句,如果您将Mac OS Xscheme添加到现有iOS项目中,testing可能不会自动添加到scheme中,因此您可能必须通过编辑scheme手动执行此操作,导航到testing部分并添加testing在那里上课。 然后,您可以通过select正确的scheme从Xcode运行这些Mac OS Xtesting,也可以从命令行执行:

     xcodebuild test -scheme NetworkTestMacOS -destination'platform = OS X,arch = x86_64'
    

    还要注意,如果你已经构build了你的目标,你可以直接运行这些testing,通过导航到正确的DerivedData文件夹(在我的例子中,它是~/Library/Developer/Xcode/DerivedData/NetworkTest-xxx/Build/Products/Debug ),然后直接从命令行运行xctest

     /Applications/Xcode.app/Contents/Developer/usr/bin/xctest -XCTest所有NetworkTestMacOSTests.xctest
    
  3. 将testing与Xcode会话隔离的另一个select是在单独的OS X服务器上进行testing。 请参阅WWDC 2013video“ Xcodetesting”中的“ 持续集成和testing”部分。 进入这里是远远超出了原来的问题的范围,所以我只是简单介绍给你的video,这个主题很好的介绍。

就我个人而言,我非常喜欢Xcode中的testing集成(它使testing的debugging变得非常简单),并且通过拥有Mac OS X目标,您可以绕过仿真器。 但是,如果你想从命令行(或OS X服务器)做到这一点,也许上述的帮助。