Unit Testing Block-based APIs

UPDATE:  I’ve changed my answer now.  The Original Post (marked as such below), represents the solution called “spinlock”.  It’s undesirable.  Have a look at this post on Grand Central Dispatch over at www.raywenderlich.com under the section “Semaphores”

The updated solution, which achieves the same result only better, can be done using a new XCTestCase baseclass for your test cases:

#import <XCTest/XCTest.h>

@interface HSAsynchronousTestCase : XCTestCase
{
   dispatch_semaphore_t _waitSemaphore;
}
- (void)HS_beginAsyncTest;
- (void)HS_completeAsyncTest;
- (void)HS_waitToComplete:(NSTimeInterval)timeoutDuration;

@end
 

@implementation HSAsynchronousTestCase

- (void)HS_beginAsyncTest
{
    _waitSemaphore = dispatch_semaphore_create(0);
}

- (void)HS_completeAsyncTest
{
    if (_waitSemaphore) {
        dispatch_semaphore_signal(_waitSemaphore);
        _waitSemaphore = nil;
    }
    
}

- (void)HS_waitToComplete:(NSTimeInterval)timeoutDuration
{
    dispatch_time_t timeoutTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t) (timeoutDuration * NSEC_PER_SEC));
    if (dispatch_semaphore_wait(_waitSemaphore, timeoutTime)) {
        XCTFail(@"Test timed out");
    }
}

- (void)setUp
{
    [super setUp];
    [self HS_completeAsyncTest]; // in case something went wrong with the last one...
}

@end

 

Then you use it analogously to the code below.  Where the _done = YES; calls are [self HS_completeAsyncTest]; and _done = NO; are the [self HS_beginAsyncTest];.

One more time, you call [self HS_beginAsyncTest]; before your asynchronous method, you call [self HS_completeAsyncTest]; in the completion block(s) of that method, and you call [self HS_waitToComplete: kSomeDurationInSeconds]; at the end of your method.

ORIGINAL POST for Reference

This is a code recipe that I’ve been using for a while and I’m sad to say I’m not even sure who I got it from. Obviously a wonderful person on stackoverflow.com.

I’m putting here for my own purposes as I tend to refer to my own posts at times. “How did I do that again…?”

The problem with any asynchronously executed code is that it finishes after the calling method does. So the method returns before your test is truly finished. We need to stop that method from returning until the asynchronously executed stuff has returned its result. This is how you do it.

Say you have a Unit Test Case subclass

@implementation SomeModel_Tests
{
    __block BOOL _done;  // add a block variable
}

/**
Then add this helper method
*/
- (BOOL)waitForCompletion:(NSTimeInterval)timeoutSecs
{
    NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeoutSecs];
    NSLog(@"Wanting to wait on thread %@", [NSThread currentThread]);
    do
    {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:timeoutDate];
        if ([timeoutDate timeIntervalSinceNow] < 0.0)
        {
            NSLog(@"Breaking out of waitForCompletion!");
            break;
        }
    }
    while (!_done);
    return _done;
}

// ...
@end

Now we have everything we need to test a block-based API. For example:

- (void)testSomeParsingOperation
{    
    NSURL *contentURL;
    contentURL = [NSURL URLWithString:@"http://www.someurl.com/content.json"];
    NSURLRequest *request = [NSURLRequest contentURL];
    
    JSONParsingRequestOperation *op;  // I just made this up.
    op = [JSONParsingRequestOperation JSONRequestWithRequest:request
                                             completionBlock:^(BOOL success, NSSet *parsedDataObjects, NSError *error)
    {
        XCTAssertTrue(error == nil, @"Parsing should have worked!");
        XCTAssertTrue(parsedDataObjects.count > 0, @"Because I know content.json should have objects in it");
        _done = YES;
    }];
    
    [[AFHTTPClient sharedClient] enqueueHTTPRequestOperation: op];
    
    [self waitForCompletion: 260];
}

That’s it. Have fun.

Advertisements