Comment tester les API asynchrones?

J’ai installé Google Toolbox pour Mac dans Xcode et suivi les instructions pour configurer les tests unitaires trouvés ici .

Tout fonctionne très bien et je peux tester mes méthodes synchrones sur tous mes objects. Cependant, la plupart des API complexes que je souhaite réellement tester les résultats de retour de manière asynchrone en appelant une méthode sur un délégué – par exemple, un appel à un système de téléchargement et de mise à jour des fichiers sera renvoyé immédiatement, puis exécutera la méthode -fileDownloadDidComplete .

Comment pourrais-je tester cela comme un test unitaire?

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

EDIT: Je suis maintenant passé à l’utilisation du système XCTest intégré à XCode et j’ai constaté que TVRSMonitor sur Github fournit un moyen facile d’utiliser des sémaphores pour attendre la fin des opérations asynchrones.

Par exemple:

- (void)testLogin { TRVSMonitor *monitor = [TRVSMonitor monitor]; __block NSSsortingng *theToken; [[Server instance] loginWithUsername:@"foo" password:@"bar" success:^(NSSsortingng *token) { theToken = token; [monitor signal]; } failure:^(NSError *error) { [monitor signal]; }]; [monitor wait]; XCTAssert(theToken, @"Getting token"); } 

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 les opérations asynchrones en un stream 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.

Je comprends que cette question a été posée et répondue il y a près d’un an, mais je ne peux m’empêcher d’être en désaccord avec les réponses données. Tester les opérations asynchrones, en particulier les opérations réseau, est une exigence très courante et importante pour réussir. Dans l’exemple donné, si vous dépendez des réponses réseau réelles, vous perdez une partie de la valeur importante de vos tests. Plus précisément, vos tests dépendent de la disponibilité et de l’exactitude fonctionnelle du serveur avec lequel vous communiquez; cette dépendance rend vos tests

  • plus fragile (que se passe-t-il si le serveur tombe en panne?)
  • moins complet (comment testez-vous systématiquement une réponse de panne ou une erreur de réseau?)
  • imaginez beaucoup plus lentement:

Les tests unitaires doivent se dérouler en fractions de seconde. Si vous devez attendre une réponse réseau de plusieurs secondes chaque fois que vous exécutez vos tests, vous risquez moins de les exécuter fréquemment.

Les tests unitaires consistent en grande partie à encapsuler des dépendances; du sharepoint vue de votre code testé, deux choses se produisent:

  1. Votre méthode lance une requête réseau, probablement en instanciant une NSURLConnection.
  2. Le délégué que vous avez spécifié reçoit une réponse via certains appels de méthode.

Votre délégué ne souhaite pas ou ne doit pas savoir d’où provient la réponse, que ce soit à partir d’une réponse réelle d’un serveur distant ou de votre code de test. Vous pouvez en profiter pour tester les opérations asynchrones en générant simplement les réponses vous-même. Vos tests s’exécuteront beaucoup plus rapidement et vous pourrez tester de manière fiable les réponses réussies ou les échecs.

Cela ne veut pas dire que vous ne devez pas exécuter de tests sur le vrai service Web sur lequel vous travaillez, mais que ces tests d’intégration font partie de leur propre suite de tests. Les défaillances de cette suite peuvent signifier que le service Web a des modifications ou est simplement arrêté. Comme ils sont plus fragiles, leur automatisation a moins de valeur que l’automatisation de vos tests unitaires.

En ce qui concerne la manière exacte de tester les réponses asynchrones à une requête réseau, vous avez deux options. Vous pouvez simplement tester le délégué isolément en appelant directement les méthodes (par exemple [someDelegate connection: connection didReceiveResponse: someResponse]). Cela fonctionnera un peu, mais est légèrement faux. Le délégué fourni par votre object peut être l’un des nombreux objects de la chaîne de delegates pour un object NSURLConnection spécifique. Si vous appelez directement les méthodes de votre délégué, il se peut que vous manquiez une fonctionnalité clé fournie par un autre délégué plus haut dans la chaîne. En guise d’alternative, vous pouvez supprimer l’object NSURLConnection que vous créez et lui faire envoyer les messages de réponse à l’ensemble de sa chaîne de delegates. Il existe des bibliothèques qui rouvriront NSURLConnection (parmi d’autres classes) et le feront pour vous. Par exemple: https://github.com/pivotal/PivotalCoreKit/blob/master/SpecHelperLib/Extensions/NSURLConnection%2BSpec.m

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 BOOL ‘downloadComplete’ 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!"); } 

Bien sûr, la boucle d’exécution pourrait fonctionner indéfiniment. Je l’améliorerai plus tard!

J’ai écrit une petite aide 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 append un délai d’attente à l’aide si vous le souhaitez,

C’est difficile. Je pense que vous devrez configurer un runloop dans votre test et aussi la possibilité de spécifier ce runloop à votre code asynchrone. Sinon, les rappels ne se produiront pas car ils sont exécutés sur un runloop.

Je suppose que vous pouvez simplement exécuter le runloop pour une courte durée en boucle. Et laissez le rappel définir une variable d’état partagé. Ou peut-être même simplement demander au rappel de terminer le runloop. De cette façon, vous savez que le test est terminé. Vous devriez pouvoir vérifier les délais d’attente en bloquant la boucle après un certain temps. Si cela se produit, un dépassement de délai s’est produit.

Je ne l’ai jamais fait mais je devrai bientôt le penser. S’il vous plaît partagez vos résultats 🙂

Si vous utilisez une bibliothèque telle que AFNetworking ou ASIHTTPRequest et que vos requêtes sont gérées via une NSOperation (ou une sous-classe avec ces bibliothèques), il est alors facile de les tester sur un serveur test / dev avec NSOperationQueue:

En test:

 // create request operation NSOperationQueue* queue = [[NSOperationQueue alloc] init]; [queue addOperation:request]; [queue waitUntilAllOperationsAreFinished]; // verify response 

Cela exécute essentiellement un runloop jusqu’à ce que l’opération soit terminée, permettant à tous les rappels de se produire sur les threads d’arrière-plan comme ils le feraient normalement.

Pour développer la solution de @ St3fan, vous pouvez essayer ceci après avoir lancé la requête:

 - (BOOL)waitForCompletion:(NSTimeInterval)timeoutSecs { NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeoutSecs]; do { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:timeoutDate]; if ([timeoutDate timeIntervalSinceNow] < 0.0) { break; } } while (!done); return done; } 

Autrement:

 //block the thread in 0.1 second increment, until one of callbacks is received. NSRunLoop *theRL = [NSRunLoop currentRunLoop]; //setup timeout float waitIncrement = 0.1f; int timeoutCounter = (int)(30 / waitIncrement); //30 sec timeout BOOL controlConditionReached = NO; // Begin a run loop terminated when the downloadComplete it set to true while (controlConditionReached == NO) { [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:waitIncrement]]; //control condition is set in one of your async operation delegate methods or blocks controlConditionReached = self.downloadComplete || self.downloadFailed ; //if there's no response - timeout after some time if(--timeoutCounter <= 0) { break; } } 

Je trouve très pratique d’utiliser https://github.com/premosystems/XCAsyncTestCase

Il ajoute trois méthodes très pratiques à XCTestCase

 @interface XCTestCase (AsyncTesting) - (void)waitForStatus:(XCTAsyncTestCaseStatus)status timeout:(NSTimeInterval)timeout; - (void)waitForTimeout:(NSTimeInterval)timeout; - (void)notify:(XCTAsyncTestCaseStatus)status; @end 

qui permettent des tests très propres. Un exemple du projet lui-même:

 - (void)testAsyncWithDelegate { NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithSsortingng:@"http://www.google.com"]]; [NSURLConnection connectionWithRequest:request delegate:self]; [self waitForStatus:XCTAsyncTestCaseStatusSucceeded timeout:10.0]; } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { NSLog(@"Request Finished!"); [self notify:XCTAsyncTestCaseStatusSucceeded]; } - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { NSLog(@"Request failed with error: %@", error); [self notify:XCTAsyncTestCaseStatusFailed]; } 

J’ai implémenté la solution proposée par Thomas Tempelmann et globalement ça fonctionne bien pour moi.

Cependant, il y a un piège. Supposons que l’unité à tester contient le code suivant:

 dispatch_async(dispatch_get_main_queue(), ^{ [self performSelector:selector withObject:nil afterDelay:1.0]; }); 

Le sélecteur peut ne jamais être appelé car le thread principal a été verrouillé jusqu’à la fin du test:

 [testBase.lock lockWhenCondition:1]; 

Globalement, nous pourrions supprimer complètement le NSConditionLock et utiliser simplement la classe GHAsyncTestCase .

Voici comment je l’utilise dans mon code:

 @interface NumericTestTests : GHAsyncTestCase { } @end @implementation NumericTestTests { BOOL passed; } - (void)setUp { passed = NO; } - (void)testMe { [self prepare]; MyTest *test = [MyTest new]; [test run: ^(NSError *error, double value) { passed = YES; [self notify:kGHUnitWaitStatusSuccess]; }]; [test runTest:fakeTest]; [self waitForStatus:kGHUnitWaitStatusSuccess timeout:5.0]; GHAssertTrue(passed, @"Completion handler not called"); } 

Beaucoup plus propre et ne bloque pas le thread principal.

Je viens d’écrire un article sur ce blog (en fait, j’ai créé un blog parce que je pensais que c’était un sujet intéressant). J’ai fini par utiliser la méthode swizzling pour pouvoir appeler le gestionnaire d’achèvement en utilisant tous les arguments que je veux sans attendre, ce qui semblait bon pour les tests unitaires. Quelque chose comme ça:

 - (void)swizzledGeocodeAddressSsortingng:(NSSsortingng *)addressSsortingng completionHandler:(CLGeocodeCompletionHandler)completionHandler { completionHandler(nil, nil); //You can test various arguments for the handler here. } - (void)testGeocodeFlagsComplete { //Swizzle the geocodeAddressSsortingng with our own method. Method originalMethod = class_getInstanceMethod([CLGeocoder class], @selector(geocodeAddressSsortingng:completionHandler:)); Method swizzleMethod = class_getInstanceMethod([self class], @selector(swizzledGeocodeAddressSsortingng:completionHandler:)); method_exchangeImplementations(originalMethod, swizzleMethod); MyGeocoder * myGeocoder = [[MyGeocoder alloc] init]; [myGeocoder geocodeAddress]; //the completion handler is called synchronously in here. //Deswizzle the methods! method_exchangeImplementations(swizzleMethod, originalMethod); STAssertTrue(myGeocoder.geocoded, @"Should flag as geocoded when complete.");//You can test the completion handler code here. } 

entrée de blog pour quiconque se soucie.

Ma réponse est que les tests unitaires, sur le plan conceptuel, ne sont pas appropriés pour tester des opérations asynchrones. Une opération asynchrone, telle qu’une demande adressée au serveur et le traitement de la réponse, se produit non pas dans une unité mais dans deux unités.

Pour associer la réponse à la demande, vous devez soit bloquer l’exécution entre les deux unités, soit gérer des données globales. Si vous bloquez l’exécution, votre programme ne s’exécute pas normalement, et si vous gérez des données globales, vous avez ajouté des fonctionnalités externes qui peuvent contenir des erreurs. Chacune de ces solutions viole la notion même de test unitaire et vous oblige à insérer un code de test spécial dans votre application. et après votre test unitaire, vous devrez toujours désactiver votre code de test et effectuer des tests “manuels” à l’ancienne. Le temps et les efforts consacrés aux tests unitaires sont alors au moins partiellement gaspillés.

J’ai trouvé cet article sur ce qui est un morceau http://dadabeatnik.wordpress.com/2013/09/12/xcode-and-asynchronous-unit-testing/