Подтвердить что ты не робот

XCTestExpectation: как избежать вызова метода выполнения после окончания контекста ожидания?

Im использует новые возможности асинхронного тестирования 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;
    }];
}

Но это кажется чересчур сложным и делает тест более трудным для чтения. Я что-то упускаю? Есть ли более простой способ решить эту проблему?

4b9b3361

Ответ 1

Да, есть намного более простой способ избежать этой проблемы с нарушением API: просто объявите переменную ожидания как __weak. Несмотря на то, что документация не была четко документирована, ожидается, что ожидание будет выпущено, когда истечет время ожидания. Поэтому, если задача занимает больше времени ожидания, переменная ожидания будет равна нулю при вызове обработчика завершения задачи. Таким образом, метод 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];
}

Ответ 2

Я столкнулся с одной и той же проблемой, но в моем случае мне понадобилась версия Swift выше.

Я работаю над OpenStack Swift Drive для OSX. Когда папка удаляется локально с помощью Finder, удаление в конечном итоге распространяется на сервер, мне нужен тест, ожидающий обновления сервера.

Чтобы избежать краха нарушения API, я изменил свои ожидания на "слабый var" и изменил вызов, чтобы выполнить его с "zeroFoldersExpectation?.fulfill()" с дополнительным "?" поскольку ожидание теперь является необязательным и может стать нулем, в этом случае вызов выполнения игнорируется. Это фиксировало сбои.

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})

}

Ответ 3

Вместо того, чтобы создавать переменную expectation как weak (как предложено в этом ответе), я думаю, что вы также можете установить переменную block и nil it в обработчике завершения 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 не будет dealloc expectation слишком быстро.