Benedict's Soapbox

Async testing with XCUTest

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

Update: 28th April ‘14

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];
    }];
}