web-dev-qa-db-ja.com

iOS 7.1でNSURLSessionを使用してHTTPリクエストとレスポンスを単体テストするにはどうすればよいですか?

NSURLSessionを使用してHTTPリクエストとレスポンスを「単体テスト」する方法を知りたいのですが。現在、単体テストとして実行すると、完了ブロックコードが呼び出されません。ただし、AppDelegate(didFinishWithLaunchingOptions)内から同じコードが実行されると、完了ブロック内のコードが呼び出されます。このスレッドで提案されているように、 NSURLSessionDataTask dataTaskWithURL完了ハンドラーが呼び出されません 、「ネットワークリクエストが完了するまでメインスレッドが確実にブロックされるようにするために」、セマフォや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 = @"[email protected]&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サーバーに依存しているという意味で、厳密な意味での「単体」テストではないかもしれません(つまり、私が求めているものは、統合テストのようなものです)。
  • 注:別のスレッドがあることに気づきました NSURLSessionを使用した単体テスト NSURLSessionを使用した単体テストについてですが、応答をモックしたくありません。
24
Jane Wayne

Xcode 6は非同期テストをwith 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ステートメントは、4つのタイプのエラーを識別します。

  1. NSErrorオブジェクトはnilではありませんでした。

  2. HTTPステータスコードは200ではありませんでした。

  3. NSDataオブジェクトはnilでした。

  4. 完了ブロックが60秒以内に完了しませんでした。

おそらく、応答の内容のテストも追加します(この単純化した例では行いませんでした)。

上記のテストは、メインキューに何かをディスパッチしている完了ブロックに何もないため、機能します。メインキューを必要とする非同期操作でこれをテストしている場合(AFNetworkingを慎重に使用しないか、手動でメインキューにディスパッチする場合に発生します)、上記のパターンでデッドロックが発生する可能性があります(ブロックしているため)ネットワーク要求の終了を待機しているメインスレッド)。しかし、NSURLSessionの場合、このパターンはうまく機能します。


シミュレーターとは独立して、コマンドラインからテストを行うことについて質問しました。いくつかの側面があります:

  1. コマンドラインからテストする場合は、コマンドラインからxcodebuildを使用できます。たとえば、コマンドラインからシミュレータでテストするには、次のようになります(この例では、私のスキームはNetworkTestと呼ばれます)。

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

    これでスキームが構築され、指定された宛先で実行されます。注:xcodebuildを使用してコマンドラインからシミュレーターでアプリをテストするXcode 5.1の問題に関する多くのレポートがあります(上記の動作をするマシンが1台あるので、別のマシンでフリーズするため、この動作を確認できます。 )。 Xcode 5.1のシミュレーターに対するコマンドラインテストは、完全に信頼できるようには見えません。

  2. テストをシミュレーターで実行したくない場合(そしてこれはコマンドラインから実行する場合でも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 
    
  3. Xcodeセッションからテストを分離する別のオプションは、別のOS Xサーバーでテストを行うことです。 WWDC 2013ビデオ Testing in Xcode継続的な統合とテストセクションを参照してください。ここに入るのは元の質問の範囲をはるかに超えているので、このトピックを紹介するビデオを紹介します。

個人的に、私はXcodeのテスト統合が非常に好きで(テストのデバッグを非常に簡単にします)、Mac OS Xターゲットを使用することで、そのプロセスでシミュレータをバイパスします。ただし、コマンドライン(またはOS Xサーバー)から実行する場合は、上記の方法が役立つ場合があります。

52
Rob