views:

399

answers:

3

Hi all.
I have installed Google Toolbox for Mac (http://code.google.com/p/google-toolbox-for-mac/) into Xcode and followed the instructions to set up unit testing found here (http://code.google.com/p/google-toolbox-for-mac/wiki/iPhoneUnitTesting).

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.

Any ideas much appreciated!

+3  A: 

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

St3fan
+2  A: 

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!

Ben Clayton
+1  A: 

I ran into the same question and found a different solution that works for me.

I use the classic approach for turning async operations into a sync flow by using a semaphore as follows:

// 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
self.theLock = [NSLock new];
[self.theLock lock];

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

// now lock the semaphore again - which will block this
// thread unless/until unlock gets invoked
[self.theLock lock];

// 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];
[conn release];
Thomas Tempelmann
LOL locks are not supposed to be used like this :)
gurghet
I bow to your superior knowledge. Would you be so good to explain what you mean?
Thomas Tempelmann
Alright, I re-read the NSLock docs and it appears that it's not "safe" to unlock a NSLock inside a thread when it was locked in a different thread. To fix this, change NSLock into NSConditionLock, remove the first [theLock lock] invocation, change the second [theLock lock] call into [theLock lockWhenCondition:1] and unlock it inside the callback with [theLock unlockWithCondition:1]
Thomas Tempelmann