64 votes

Comment tester des API asynchrones?

J'ai installé Google Toolbox for Mac dans Xcode et suivi les instructions de mise en place de tests unitaires trouvé ici.

Tout fonctionne très bien, et je peux tester mes méthodes synchrones sur tous mes objets tout à fait bien. Cependant, la plupart des complexes Api je veux tester le retour des résultats de manière asynchrone via l'appel d'une méthode sur un délégué par exemple un appel à un fichier télécharger et mettre à jour le système retourne immédiatement et puis exécutez une -fileDownloadDidComplete: méthode a la fin du téléchargement.

Comment pourrais-je tester ce qu'un test unitaire?

Il me semble que je serais à la testDownload fonction, ou au moins le framework de test à attendre fileDownloadDidComplete: méthode à exécuter.

EDIT: j'ai maintenant passé à l'aide de l'XCode intégré XCTest système et ont constaté que la TVRSMonitor sur Github fournit une mort facile à utiliser des sémaphores à attendre pour les opérations asynchrones pour terminer.

Par exemple:

- (void)testLogin {
  TRVSMonitor *monitor = [TRVSMonitor monitor];
  __block NSString *theToken;

  [[Server instance] loginWithUsername:@"foo" password:@"bar"
                               success:^(NSString *token) {
                                   theToken = token;
                                   [monitor signal];
                               }

                               failure:^(NSError *error) {
                                   [monitor signal];
                               }];

  [monitor wait];

  XCTAssert(theToken, @"Getting token");
}

52voto

Thomas Tempelmann Points 2849

J'ai rencontré la même question et trouvé une solution différente qui fonctionne pour moi.

J'utilise l'approche "old school" pour transformer des opérations asynchrones en un flux de synchronisation en utilisant un sémaphore comme suit:

 // create the object that will perform an async operation
MyConnection *conn = [MyConnection new];
STAssertNotNil (conn, @"MyConnection init failed");

// create the semaphore and lock it once before we start
// the async operation
NSConditionLock *tl = [NSConditionLock new];
self.theLock = tl;
[tl release];    

// start the async operation
self.testState = 0;
[conn doItAsyncWithDelegate:self];

// now lock the semaphore - which will block this thread until
// [self.theLock unlockWithCondition:1] gets invoked
[self.theLock lockWhenCondition:1];

// make sure the async callback did in fact happen by
// checking whether it modified a variable
STAssertTrue (self.testState != 0, @"delegate did not get called");

// we're done
[self.theLock release]; self.theLock = nil;
[conn release];
 

Assurez-vous d'invoquer

 [self.theLock unlockWithCondition:1];
 

Dans le (s) délégué (s) alors.

44voto

Adam Milligan Points 2450

J'apprécie le fait que cette question a été posée et a répondu à près d'un an, mais je ne peux pas aider mais être en désaccord avec les réponses données. Les tests des opérations asynchrones, en particulier les opérations de réseau, est un très commun, et est important pour obtenir le droit. Dans l'exemple donné, si vous dépendez du réseau réel des réponses de vous faire perdre de la valeur importante de vos tests. Plus précisément, vos tests de devenir dépendant de la disponibilité et de la correction fonctionnelle du serveur que vous êtes en communication avec; cette dépendance rend vos tests

  • de plus en plus fragile (ce qui se passe si le serveur tombe en panne?)
  • moins complet (comment voulez-vous systématiquement le test d'une réponse en cas d'échec ou d'erreur de réseau?)
  • significativement plus lent imaginer de tester ce:

Les tests unitaires doivent exécuter en une fraction de seconde. Si vous devez attendre pour un multi-deuxième réponse du réseau à chaque fois que vous exécutez vos tests, alors vous êtes moins susceptibles de les exécuter fréquemment.

Le test unitaire est en grande partie sur l'encapsulation des dépendances; du point de vue de votre code à tester, il se passe deux choses:

  1. Votre méthode déclenche une demande du réseau, probablement par l'instanciation d'un NSURLConnection.
  2. Le délégué vous avez spécifié reçoit une réponse par l'intermédiaire de certains appels de méthode.

Votre délégué n'est pas, ou ne devrait pas, de soins où la réponse est venue, qu'il s'agisse d'une réponse réelle à partir d'un serveur distant ou à partir de votre code de test. Vous pouvez en profiter pour tester les opérations asynchrones par le simple fait de produire les réponses vous-même. Vos tests s'exécute beaucoup plus rapidement, et vous pouvez de manière fiable test de la réussite ou de l'échec des réponses.

Ce n'est pas dire que vous ne devriez pas exécuter des tests à l'encontre de la véritable service web que vous travaillez avec, mais ce sont les tests d'intégration et appartiennent à leur propre suite de tests. Les échecs dans la suite peut signifier que le service web a des changements, ou simplement vers le bas. Car ils sont plus fragiles, l'automatisation de leur tendance à avoir moins de valeur que l'automatisation des tests unitaires.

Concernant exactement comment aller sur les tests asynchrones réponses à une demande du réseau, vous avez quelques options. Vous pourriez tout simplement tester le délégué à l'isolement en appelant les méthodes directement (par exemple, [someDelegate connexion:connexion didReceiveResponse:someResponse]). Cela fonctionne un peu, mais un peu de mal. Le délégué de votre objet fournit peut-être juste un de plusieurs objets dans la déléguée de la chaîne pour un NSURLConnection objet; si vous appelez votre délégué des méthodes directement, vous pouvez manquer de certains élément clé de la fonctionnalité fournie par un autre délégué, en aval de la chaîne. Comme une meilleure alternative, vous pouvez talon la NSURLConnection objet de créer et de rediriger les messages de réponse à l'ensemble de son délégué de la chaîne. Il y a des bibliothèques qui va rouvrir NSURLConnection (parmi d'autres classes) et de le faire pour vous. Par exemple: https://github.com/pivotal/PivotalCoreKit/blob/master/SpecHelperLib/Extensions/NSURLConnection%2BSpec.m

19voto

Ben Clayton Points 16793

St3fan, tu es un génie. Merci beaucoup!

Voici comment je l'ai fait en utilisant votre suggestion.

'Downloader' définit un protocole avec une méthode DownloadDidComplete qui se déclenche à la fin. Il existe une variable membre 'downloadComplete' BOOL qui est utilisée pour terminer la boucle d'exécution.

 -(void) testDownloader {
 downloadComplete = NO;
 Downloader* downloader = [[Downloader alloc] init] delegate:self];

 // ... irrelevant downloader setup code removed ...

 NSRunLoop *theRL = [NSRunLoop currentRunLoop];

 // Begin a run loop terminated when the downloadComplete it set to true
 while (!downloadComplete && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);

}


-(void) DownloaderDidComplete:(Downloader*) downloader withErrors:(int) errors {
    downloadComplete = YES;

    STAssertNotEquals(errors, 0, @"There were errors downloading!");
}
 

La boucle d’exécution pourrait potentiellement fonctionner indéfiniment. Je l’améliorerai plus tard!

16voto

Holtwick Points 812

J'ai écrit un petit assistant qui facilite le test des API asynchrones. D'abord l'assistant:

 static inline void hxRunInMainLoop(void(^block)(BOOL *done)) {
    __block BOOL done = NO;
    block(&done);
    while (!done) {
        [[NSRunLoop mainRunLoop] runUntilDate:
            [NSDate dateWithTimeIntervalSinceNow:.1]];
    }
}
 

Vous pouvez l'utiliser comme ceci:

 hxRunInMainLoop(^(BOOL *done) {
    [MyAsyncThingWithBlock block:^() {
        /* Your test conditions */
        *done = YES;
    }];
});
 

Il ne continuera que si done devient TRUE , alors assurez-vous de le définir une fois terminé. Bien sûr, vous pouvez ajouter un délai d'attente à l'aide si vous le souhaitez,

8voto

St3fan Points 16196

C'est délicat. Je pense que vous aurez besoin de configurer un runloop dans votre test et également la possibilité de spécifier que runloop à votre async code. Sinon, les rappels n'arrivera pas car elles sont exécutées sur un runloop.

Je suppose que vous pouvez simplement exécuter le runloop pour s courte durée dans une boucle. Et de laisser le rappel partage, la variable d'état. Ou peut-être même tout simplement de demander le rappel de résilier le runloop. De cette façon, vous vous le savez, le test est terminé. Vous devriez être en mesure de vérifier les délais d'attente par stoppng la boucle après un certain temps. Si cela se produit, un délai d'attente se sont produits.

Je n'ai jamais fait cela, mais je vais devoir bientôt je pense. S'il vous plaît partagez vos résultats :-)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X