Let me start with a confession: I’m not much of a tester. I don’t practice test driven development and I don’t include tests as a build step. I’m certainly not anti-testing, but I am yet to see evidence that it would help me write significantly better code. I’d like to work on a project that shows me the error of my ways but that has yet to happen.
But I do still write tests. I like that Xcode makes it easy to add tests (if it weren’t easy there’s even less chance that I’d write them). Often, however, the functionality that I want to test is asynchronous. Xcode’s build in test tools do not provide explicit support asynchronous testing. I’ve written 2 small functions which make it easy to write asynchronous tests using Xcode’s build in test tools:
#pragma mark - async functions
static void FIRE_RUNLOOP_UNTIL(BOOL(^condition)(void), NSTimeInterval relativeTimeout) {
NSTimeInterval absoluteTimeout = [NSDate timeIntervalSinceReferenceDate] + relativeTimeout;
while (!condition()) {
BOOL didTimeout = absoluteTimeout < [NSDate timeIntervalSinceReferenceDate];
if (didTimeout) return;
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
}
}
I keep these as Xcode snippets. When I want to write an asynchronous test I drag the snippet to the top of the test case file.
Use them like so:
-(void)testRetainCycleIsAvoided
{
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{}];
__block BOOL didComplete = NO;
[operation EMK_setCompletionBlockUsingDispatchQueue:NULL block:^(NSOperation *operation) {
didComplete = YES;
}];
NSOperationQueue *queue = [NSOperationQueue new];
[queue addOperation:operation];
FIRE_RUNLOOP_UNTIL(^BOOL{
return didComplete;
}, 1);
XCTAssertTrue(didComplete, @"Failed to complete operation (therefore subsequent tests are uninformative).");
XCTAssertTrue(queue.operationCount == 0, @"Operation still enqueued (therefore subsequent tests are uninformative).");
}
I’m not sure why I implemented these as functions; methods seems like a better choice:
#pragma mark - run loop firing
-(void)fireRunLoopWithTimeout:(NSTimeInterval)relativeTimeout expirationBlock:(BOOL(^)(void))condition
{
NSTimeInterval absoluteTimeout = [NSDate timeIntervalSinceReferenceDate] + relativeTimeout;
while (!condition()) {
BOOL didTimeout = absoluteTimeout < [NSDate timeIntervalSinceReferenceDate];
if (didTimeout) return;
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
}
}
-(void)fireRunLoopForDuration:(NSTimeInterval)relativeTimeout
{
NSTimeInterval absoluteTimeout = [NSDate timeIntervalSinceReferenceDate] + relativeTimeout;
[self fireRunLoopWithTimeout:relativeTimeout expirationBlock:^BOOL{
return absoluteTimeout < [NSDate timeIntervalSinceReferenceDate];
}];
}