web-dev-qa-db-fra.com

XCTestExpectation: comment éviter d'appeler la méthode execute une fois le contexte d'attente terminé?

J'utilise les nouvelles capacités de test asynchrone de Xcode 6. Tout fonctionne correctement lorsque la tâche asynchrone se termine avant le délai d'expiration. Mais si la tâche prend plus de temps que le délai d'attente, les choses se compliquent.

Voici comment je fais mes tests:

@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

Malheureusement, l'appel de la méthode fulfill une fois le délai d'expiration bloque la suite de tests avec cette erreur:

Violation de l'API - appelée - [XCTestExpectation remplir] après la fin du contexte d'attente.

*** 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

Bien sûr, je peux vérifier si le test est terminé avant d'appeler la méthode fulfill comme ceci:

- (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;
    }];
}

Mais cela semble trop compliqué et rend le test beaucoup plus difficile à lire. Suis-je en train de manquer quelque chose? Existe-t-il un moyen plus simple de résoudre ce problème?

22
0xced

Oui, il existe un moyen beaucoup plus simple d'éviter ce problème de violation d'API: déclarez simplement votre variable d'attente comme __weak. Bien que cela ne soit pas clairement documenté, l'attente sera libérée à l'expiration du délai. Ainsi, si la tâche prend plus de temps que le délai d'expiration, la variable d'attente sera nulle lorsque le gestionnaire de fin de tâche est appelé. Ainsi, la méthode fulfill sera appelée sur nil, sans rien faire.

- (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

Je suis tombé sur le même problème mais dans mon cas, j'avais besoin de la version Swift de la réponse ci-dessus.

Je travaille sur un OpenStack Swift Drive for OSX. Lorsqu'un dossier est supprimé localement avec le Finder, la suppression se propage finalement au serveur, j'avais besoin d'un test qui attend que le serveur soit mis à jour .

Pour éviter le plantage de violation d'API, j'ai changé mes attentes pour être "var faible" et changé l'appel pour le remplir en "zeroFoldersExpectation? .Fulfill ()" avec le supplément "?" car l'attente est désormais facultative et pourrait devenir nulle, auquel cas l'appel à la fonction est ignoré. Cela a corrigé les plantages.

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

Au lieu de créer expectation comme variable weak (comme suggéré dans cette réponse ) je pense que vous pouvez également définir la variable block et rien dans gestionnaire de complétion de 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;
    }];
}

De cette façon, vous êtes sûr qu'ARC ne désallouera pas expectation trop rapidement.

6
Piotr