Asynchronous Testing with SenTestingKit

Xcode includes a testing library which is useful for unit testing.  Simple unit tests are easy to manage, but it tends to be (A) perform operation, and (B) test the state of your objects.

I was recently doing some asynchronous programming and wanted to build some tests against my code.  A little searching led me to a good idea by akisute on Github.  But I thought it could use some simplification.

It boils down to a single method that takes a block to determine if the expected action is completed. Simply implement it to return YES when it is.

- (NSTimeInterval)waitTillReady:(BOOL (^)())isReady timeout:(NSTimeInterval)seconds {
  NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate];
  NSDate *loopUntil = [NSDate dateWithTimeIntervalSinceNow:seconds];

  while (isReady() == NO && [loopUntil timeIntervalSinceNow] > 0) {
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:loopUntil];
  }

  NSTimeInterval finish = [NSDate timeIntervalSinceReferenceDate];
  return (finish - start);
}

In line 6 above, NSRunLoop performs the magic. In any iOS or OS X there is a run loop at the top of the call stack that gets things running in the event-driven applications. Instead of allowing it to drop out of our call stack back to the top level, we are creating a secondary run loop that allows anything the user causes or external events cause to be processed.

Once our event occurs or the timeout is reached, we return and let the normal execution of the main run loop eventually take back over.

And here’s an example use of it. Creating a message object which is simply loading a URL from the web. Here’s the code to test that it starts loading within 5 seconds, and that it completes within 20 seconds.

- (void)testMessage {
  PPPostMessage *message = [[PPPostMessage alloc] init];
  STAssertTrue(message.isRequesting, @"should automatically start requesting (currently)");

  NSTimeInterval delay = [self waitTillReady:^BOOL{
    return message.isLoading;
  } timeout:5];
  STAssertTrue(message.isLoading, @"expecting it to start loading in 5 second");
  NSLog(@"[%@ %@] load statup delay = %f", NSStringFromClass([self class]), NSStringFromSelector(_cmd), delay );

  delay = [self waitTillReady:^BOOL{
      return message.isLoaded;
    } timeout:20 - delay];
  
  STAssertTrue(message.isLoaded, @"expecting it to be loaded in 20 seconds");
  NSLog(@"[%@ %@] load time = %f", NSStringFromClass([self class]), NSStringFromSelector(_cmd), delay );
}

You’ll notice that the waitTillReady: method returns the time that it actually took and we are using that value to adjust the time we allow on the second test.