ios - 如何在 iOS 7.1 中使用 NSURLSession 对 HTTP 请求和响应进行单元测试?

标签 ios objective-c nsurlsession xctest nsurlsessiontask

我想知道如何使用 NSURLSession 对 HTTP 请求和响应进行“单元测试”。现在,作为单元测试运行时,我的完成块代码不会被调用。但是,当从 AppDelegate 中执行相同的代码时(didFinishWithLaunchingOptions),调用完成块内的代码。正如此线程中所建议的,NSURLSessionDataTask dataTaskWithURL completion handler not getting called ,需要使用信号量和/或 dispatch_group 来“确保主线程被阻塞,直到网络请求完成。”

我的 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

测试此方法的方法如下所示。
- (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];
}

关于如何在单元测试中运行时执行完成块的任何指示?我是 iOS 的新手,所以一个明确的例子真的很有帮助。
  • 注意:我意识到我所要求的也可能不是严格意义上的“单元”测试,因为我依赖 HTTP 服务器作为我测试的一部分(意思是,我所要求的更像是集成测试)。
  • 注意:我意识到还有另一个线程 Unit tests with NSURLSession关于使用 NSURLSession 进行单元测试,但我不想模拟响应。
  • 最佳答案

    Xcode 6 现在使用 XCTestExpectation 处理异步测试.当测试一个异步进程时,你建立这个进程将异步完成的“期望”,在发出异步进程后,你然后等待期望被满足一段固定的时间,当查询完成时,你将异步实现期望。

    例如:

    - (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 ,但我会出于历史目的保留它。

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

    例如:
    - (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");
    }
    

    信号量将确保在请求完成之前测试不会完成。

    显然,我上面的测试只是发出一个随机的 HTTP 请求,但希望它说明了这个想法。还有我的各种XCTAssert语句将识别四种类型的错误:
  • NSError对象不为零。
  • HTTP 状态代码不是 200。
  • NSData对象为零。
  • 完成块没有在 60 秒内完成。

  • 您可能还会为响应的内容添加测试(在这个简化的示例中我没有这样做)。

    请注意,上述测试有效,因为我的完成块中没有任何内容向主队列发送任何内容。如果您正在使用需要主队列的异步操作进行测试(如果您不小心使用 AFNetworking 或自己手动分派(dispatch)到主队列,就会发生这种情况),您可能会遇到上述模式的死锁(因为我们正在阻塞主线程等待网络请求完成)。但在 NSURLSession 的情况下,这个模式很好用。

    您询问了从命令行进行测试,独立于模拟器。有几个方面:
  • 如果要从命令行进行测试,可以使用 xcodebuild从命令行。例如,要从命令行在模拟器上进行测试,它将是(在我的示例中,我的方案称为 NetworkTest ):

    xcodebuild test -scheme NetworkTest -destination 'platform=iOS Simulator,name=iPhone Retina (3.5-inch),OS=7.0'

    这将构建方案并在指定的目标上运行它。请注意,有许多关于 Xcode 5.1 问题的报告,使用 xcodebuild 从命令行在模拟器上测试应用程序| (我可以验证这种行为,因为我有一台机器,上面的工作正常,但它在另一台机器上卡住)。在 Xcode 5.1 中针对模拟器的命令行测试似乎并不完全可靠。
  • 如果您不希望您的测试在模拟器上运行(无论是从命令行还是从 Xcode 进行测试,这都适用),那么您可以构建一个 MacOS X 目标,并为该构建提供相关的方案。例如,我向我的应用程序添加了一个 Mac OS X 目标,然后添加了一个名为 NetworkTestMacOS 的方案。为了它。

    顺便说一句,如果您将 Mac OS X 方案添加到现有的 iOS 项目,则测试可能不会自动添加到该方案中,因此您可能必须通过编辑方案、导航到测试部分并添加您的测试来手动执行此操作在那里上课。然后,您可以通过选择正确的方案从 Xcode 运行这些 Mac OS X 测试,或者您也可以从命令行执行它:

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

    另请注意,如果您已经构建了目标,则可以通过导航到右侧 DerivedData 来直接运行这些测试。文件夹(在我的例子中,它是 ~/Library/Developer/Xcode/DerivedData/NetworkTest-xxx/Build/Products/Debug )然后运行 ​​xctest直接从命令行:

    /Applications/Xcode.app/Contents/Developer/usr/bin/xctest -XCTest All NetworkTestMacOSTests.xctest
  • 将测试与 Xcode session 隔离的另一种选择是在单独的 OS X 服务器上进行测试。请参阅 WWDC 2013 视频 Testing in Xcode 的持续集成和测试部分.在这里深入探讨这个问题远远超出了原始问题的范围,所以我只会向您介绍该视频,该视频很好地介绍了该主题。

  • 就我个人而言,我真的很喜欢 Xcode 中的测试集成(它使测试的调试变得非常容易)并且通过拥有 Mac OS X 目标,您可以在该过程中绕过模拟器。但是,如果您想从命令行(或 OS X Server)执行此操作,则上述方法可能会有所帮助。

    关于ios - 如何在 iOS 7.1 中使用 NSURLSession 对 HTTP 请求和响应进行单元测试?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23657486/

    相关文章:

    ios - NSPredicate with core data, search word with boundaries in string 属性

    ios - 拖动单元格时如何在 UITableViewCell 中隐藏阴影

    objective-c - 处理回调

    ios - ADInterstitialAd 不适用于 ipad swift

    iphone - 如何使用NSXMLParser解析同名父子元素

    iphone - 如何解析Json对象

    ios - NSURLSession 生命周期和基本授权

    Swift:Alamofire HTTP 代理

    ios - 使用 NSURLSessionDownloadTask 时使用 resumeData 的正确方法是什么?

    ios - NSUserDefaults 不适用于 Swift 中的 UITableView