web-dev-qa-db-ja.com

XCTestExpectation:待機コンテキストが終了した後にフルフィルメソッドを呼び出さないようにする方法

Xcode 6の新しい非同期テスト機能を使用しています。非同期タスクがタイムアウト前に終了すると、すべてが正常に機能します。ただし、タスクがタイムアウトよりも長くかかる場合、状況はさらに複雑になります。

テストの方法は次のとおりです。

@interface AsyncTestCase : XCTestCase @end

@implementation AsyncTestCase

// The asynchronous task would obviously be more complex in a real world scenario.
- (void) startAsynchronousTaskWithDuration:(NSTimeInterval)duration completionHandler:(void (^)(id result, NSError *error))completionHandler
{
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        completionHandler([NSObject new], nil);
    });
}

- (void) test1TaskLongerThanTimeout
{
    XCTestExpectation *expectation = [self expectationWithDescription:@"Test 1: task longer than timeout"];
    [self startAsynchronousTaskWithDuration:4 completionHandler:^(id result, NSError *error) {
        XCTAssertNotNil(result);
        XCTAssertNil(error);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:2 handler:nil];
}

- (void) test2TaskShorterThanTimeout
{
    XCTestExpectation *expectation = [self expectationWithDescription:@"Test 2: task shorter than timeout"];
    [self startAsynchronousTaskWithDuration:5 completionHandler:^(id result, NSError *error) {
        XCTAssertNotNil(result);
        XCTAssertNil(error);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:10 handler:nil];
}

@end

残念ながら、タイムアウトの期限が切れた後にfulfillメソッドを呼び出すと、次のエラーでテストスイートがクラッシュします。

API違反-待機コンテキストが終了した後に-[XCTestExpectationフルフィル]と呼ばれます。

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'API violation - called -[XCTestExpectation fulfill] after the wait context has ended.'
*** First throw call stack:
(
  0   CoreFoundation   0x000000010c3a6f35 __exceptionPreprocess + 165
  1   libobjc.A.dylib  0x000000010a760bb7 objc_exception_throw + 45
  2   CoreFoundation   0x000000010c3a6d9a +[NSException raise:format:arguments:] + 106
  3   Foundation       0x000000010a37d5df -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 195
  4   XCTest           0x0000000115c48ee1 -[XCTestExpectation fulfill] + 264
  ...
)
libc++abi.dylib: terminating with uncaught exception of type NSException

もちろん、次のようにfulfillメソッドを呼び出す前に、テストが終了したかどうかを確認できます。

- (void) test1TaskLongerThanTimeout
{
    XCTestExpectation *expectation = [self expectationWithDescription:@"Test 1: task longer than timeout"];

    __block BOOL testIsFinished = NO;
    [self startAsynchronousTaskWithDuration:4 completionHandler:^(id result, NSError *error) {
        if (testIsFinished) {
            return;
        }
        XCTAssertNotNil(result);
        XCTAssertNil(error);
        [expectation fulfill];
    }];

    [self waitForExpectationsWithTimeout:2 handler:^(NSError *error) {
        testIsFinished = YES;
    }];
}

しかし、これは過度に複雑に見え、テストを読みにくくします。何か不足していますか?この問題を解決する簡単な方法はありますか?

22
0xced

はい、このAPI違反の問題を回避する非常に簡単な方法があります:期待変数を__weakと宣言するだけです。明確に文書化されていませんが、タイムアウトの期限が切れると期待値が解放されます。したがって、タスクがタイムアウトよりも長くかかる場合、タスク完了ハンドラーが呼び出されたときに期待変数はnilになります。したがって、fulfillメソッドはnilで呼び出され、何もしません。

- (void) test1TaskLongerThanTimeout
{
    __weak XCTestExpectation *expectation = [self expectationWithDescription:@"Test 1: task longer than timeout"];
    [self startAsynchronousTaskWithDuration:4 completionHandler:^(id result, NSError *error) {
        XCTAssertNotNil(result);
        XCTAssertNil(error);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:2 handler:nil];
}
45
0xced

私は同じ問題に遭遇しましたが、私の場合、Swift上記の回答のバージョンが必要でした。

私はOpenStackに取り組んでいますSwift Drive for OSX。フォルダがFinderでローカルに削除されると、削除は最終的にサーバーに伝播します。サーバーが更新されるのを待つテストが必要でした。

API違反のクラッシュを回避するために、期待を「弱いvar」に変更し、それを満たすための呼び出しを「zeroFoldersExpectation?.fulfill()」に追加して「?」を追加しました。期待値はオプションになり、nilになる可能性があるため、フルフィルコールは無視されます。これはクラッシュを修正しました。

func testDeleteFolder()
{
    Finder.deleteFolder()

    weak var zeroFoldersExpectation=expectationWithDescription("server has zero folders")
    Server.waitUntilNServerFolders(0, withPrefix: "JC/TestSwiftDrive/", completionHandler: {zeroFoldersExpectation?.fulfill()})
    waitForExpectationsWithTimeout(10, handler: {error in})

}
12
Jorge Costa

expectationweak変数として作成する代わりに( この答え で提案されているように)block変数として設定して、nilにすることもできますwaitForExpectationsWithTimeoutの完了ハンドラ:

- (void) test1TaskLongerThanTimeout
{
    __block XCTestExpectation *expectation = [self expectationWithDescription:@"Test 1: task longer than timeout"];
    [self startAsynchronousTaskWithDuration:4 completionHandler:^(id result, NSError *error) {
        XCTAssertNotNil(result);
        XCTAssertNil(error);
        [expectation fulfill];
    }];
    [self waitForExpectationsWithTimeout:2 handler:^(NSError *error) {
        expectation = nil;
    }];
}

これにより、ARCがexpectationの割り当てを高速に解除しないことが保証されます。

6
Piotr