How to unit test asynchronous APIs?

Viewed 27325

I have installed Google Toolbox for Mac into Xcode and followed the instructions to set up unit testing found here.

It all works great, and I can test my synchronous methods on all my objects absolutely fine. However, most of the complex APIs I actually want to test return results asynchronously via calling a method on a delegate - for example a call to a file download and update system will return immediately and then run a -fileDownloadDidComplete: method when the file finishes downloading.

How would I test this as a unit test?

It seems like I'd want to the testDownload function, or at least the test framework to 'wait' for fileDownloadDidComplete: method to run.

EDIT: I've now switched to using the XCode built-in XCTest system and have found that TVRSMonitor on Github provides a dead easy way to use semaphores to wait for async operations to complete.

For example:

- (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");
}
13 Answers

St3fan, you are a genius. Thanks a lot!

This is how I did it using your suggestion.

'Downloader' defines a protocol with a method DownloadDidComplete that fires on completion. There's a BOOL member variable 'downloadComplete' that is used to terminate the run loop.

-(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!");
}

The run-loop could potentially run forever of course.. I'll improve that later!

This is tricky. I think you will need to setup a runloop in your test and also the ability to specify that runloop to your async code. Otherwise the callbacks won't happen since they are executed on a runloop.

I guess you could just run the runloop for s short duration in a loop. And let the callback set some shared status variable. Or maybe even simply ask the callback to terminate the runloop. That way you you know the test is over. You should be able to check for timeouts by stoppng the loop after a certain time. If that happens then a timeout ocurred.

I've never done this but I will have to soon I think. Please do share your results :-)

Related