25

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 = @"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 サーバーに依存しているという点で、厳密な意味での「単体」テストではない可能性があることを認識しています (つまり、私が求めているのは統合テストのようなものです)。
  • 注: NSURLSession を使用した単体テストに関する別のスレッドUnit tests with NSURLSessionがあることは認識していますが、応答をモックしたくありません。
4

1 に答える 1

53

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ステートメントは、次の 4 種類のエラーを識別します。

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

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

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

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

おそらく、応答の内容に対するテストも追加することになるでしょう (この簡略化された例では実行しませんでした)。

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


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

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

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

    これにより、スキームが構築され、指定された宛先で実行されます。Xcode 5.1 で、コマンド ラインからシミュレーターでアプリをテストする際に問題が発生するという報告が多数あることに注意してくださいxcodebuild(上記が正常に動作するマシンが 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 すべて NetworkTestMacOSTests.xctest
    
  3. Xcode セッションからテストを分離する別のオプションは、別の OS X サーバーでテストを行うことです。WWDC 2013 ビデオTesting in XcodeのContinuous Integration and Testingセクションを参照してください。ここに入るには、元の質問の範囲をはるかに超えているため、トピックの紹介を提供するビデオを参照するだけです。

個人的には、Xcode でのテストの統合 (テストのデバッグが非常に簡単になります) がとても気に入っています。また、Mac OS X をターゲットにすることで、そのプロセスでシミュレーターをバイパスできます。しかし、コマンド ライン (または OS X Server) から実行したい場合は、上記の方法が役立つかもしれません。

于 2014-05-14T15:01:48.533 に答える