I implemented the solution proposed by Thomas Tempelmann and overall it works greatfine for me.
However, if there's a bug it may be that the delegatethere is not called bya gotcha. Suppose the unit to be tested, resulting in this line not being called contains the following code:
dispatch_async(dispatch_get_main_queue(), ^{
[self.theLock unlockWithConditionperformSelector:1];selector withObject:nil afterDelay:1.0];
});
This can stall the execution of all tests which is a no-no in a CI environment.
To solve this, I came up with the solution of creating an external interface responsible for lockingThe selector may never be called as we told the NSConditionLock, and unlocking it on a separatemain thread after a given timeout.
Interface fileto lock until the test completes:
#import <Foundation/Foundation.h>
/**
* Interface providing a lock to be used to transform asynchronous tests into synchronous ones
* Tests should instance this lock and wait on condition before resuming execution[testBase.
* This lock is guaranteed to be unlocked automatically after a given timeout if the test doesn't complete,
* so that tests execution is not stalled
*/
@interface AsyncTestBase lockWhenCondition: NSObject
@property NSConditionLock *lock;
@end1];
Implementation fileOverall, we could get rid of the NSConditionLock altogether and simply use the GHAsyncTestCase class instead.
This is how I use it in my code:
#import "AsyncTestBase.h"
// Uncomment this to avoid thread unlock durring debugging
//#define BREAKPOINTS_ENABLED
#define kLongDelay 5.0f
@implementation AsyncTestBase {
NSThread *thread;
}
@synthesize lock =@interface _lock;
-NumericTestTests (void)setLock:(NSConditionLock *)lock {
_lock = lock;
#ifndef BREAKPOINTS_ENABLED
[self startThread];
#endif
}
- (NSConditionLock *)lock {
return _lock;
}
- (void)startThreadGHAsyncTestCase {
thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMain) object:nil];
[thread start];
}
- (void)stopThread {
NSLog(@"%s *** TIMER EXPIRED ***", __func__);
if (_lock != nil) {
@end
[_lock unlockWithCondition:1];
@implementation }
}
-NumericTestTests (void)threadMain
{
// Add selector to prevent CFRunLoopRunInMode from returning immediately
[self performSelector:@selector(stopThread) withObject:nil afterDelay:kLongDelay];
BOOL done = NO;
do
{
// Start the run loop but return after each source is handled.
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
// If a source explicitly stopped the run loop, or if there are no
// sources or timers, go ahead and exit.
if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
done = YES;
}
while (!done);passed;
}
@end
The unit test can then call:
- (void)setUp
{
testBasepassed = [AsyncTestBase new];NO;
}
- (void)testMe {
// create the semaphore and lock it once before we start
// the async operation
NSConditionLock *tl = [NSConditionLock new];
testBase.lock = tl;
__block BOOL passed =[self NO;prepare];
NumericTestMyTest *test = [NumericTest[MyTest new];
// Run test asynchronously
[test runTestrun: ^(NSError *error, double value) {
passed = YES;
[self->testBase.lock unlockWithConditionnotify:1];kGHUnitWaitStatusSuccess];
}];
[test runTest:fakeTest];
// now lock the semaphore - which will block this thread until
// [self.theLock unlockWithConditionwaitForStatus:1] gets invoked
[testBase.lockkGHUnitWaitStatusSuccess lockWhenConditiontimeout:1];
// make sure the async callback did in fact happen by5.0];
// checking whether it modified a variable
GHAssertTrue(passed, @"Completion handler not called");
testBase.lock = nil;
}
Much cleaner and doesn't block the main thread.