Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pattern for unit testing async queue that calls main queue on completion

This is related to my previous question, but different enough that I figured I'd throw it into a new one. I have some code that runs async on a custom queue, then executes a completion block on the main thread when complete. I'd like to write unit test around this method. My method on MyObject looks like this.

+ (void)doSomethingAsyncThenRunCompletionBlockOnMainQueue:(void (^)())completionBlock {      dispatch_queue_t customQueue = dispatch_queue_create("com.myObject.myCustomQueue", 0);      dispatch_async(customQueue, ^(void) {          dispatch_queue_t currentQueue = dispatch_get_current_queue();         dispatch_queue_t mainQueue = dispatch_get_main_queue();          if (currentQueue == mainQueue) {             NSLog(@"already on main thread");             completionBlock();         } else {             dispatch_async(mainQueue, ^(void) {                 NSLog(@"NOT already on main thread");                 completionBlock();         });      } }); 

}

I threw in the main queue test for extra safety, but It always hits the dispatch_async. My unit test looks like the following.

- (void)testDoSomething {      dispatch_semaphore_t sema = dispatch_semaphore_create(0);      void (^completionBlock)(void) = ^(void){                 NSLog(@"Completion Block!");         dispatch_semaphore_signal(sema);     };       [MyObject doSomethingAsyncThenRunCompletionBlockOnMainQueue:completionBlock];      // Wait for async code to finish     dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);     dispatch_release(sema);      STFail(@"I know this will fail, thanks"); } 

I create a semaphore in order to block the test from finishing before the async code does. This would work great if I don't require the completion block to run on the main thread. However, as a couple folks pointed out in the question I linked to above, the fact that the test is running on the main thread and then I enqueue the completion block on the main thread means I'll just hang forever.

Calling the main queue from an async queue is a pattern I see a lot for updating the UI and such. Does anyone have a better pattern for testing async code that calls back to the main queue?

like image 369
Drewsmits Avatar asked Oct 19 '11 06:10

Drewsmits


People also ask

What is the test method to test an asynchronous operation in XCTest?

XCTest provides two approaches for testing asynchronous code. For Swift code that uses async and await for concurrency, you mark your test methods async or async throws to test asynchronously.

Can unit tests be async?

Async void unit test methods don't provide an easy way for their unit test framework to retrieve the results of the test. In spite of this difficulty, some unit test frameworks do support async void unit tests by providing their own SynchronizationContext in which its unit tests are executed.

Why is it important for unit tests to run quickly?

The speed of detecting non-working code depends on the tools used for continuous integration. Tests can be set to run either a one-time check at a certain time interval or can be run immediately in real-time to review changes. In short, unit tests help developers detect problems immediately, then fix them quickly.


1 Answers

There are two ways to get blocks dispatched to the main queue to run. The first is via dispatch_main, as mentioned by Drewsmits. However, as he also noted, there's a big problem with using dispatch_main in your test: it never returns. It will just sit there waiting to run any blocks that come its way for the rest of eternity. That's not so helpful for a unit test, as you can imagine.

Luckily, there's another option. In the COMPATIBILITY section of the dispatch_main man page, it says this:

Cocoa applications need not call dispatch_main(). Blocks submitted to the main queue will be executed as part of the "common modes" of the application's main NSRunLoop or CFRunLoop.

In other words, if you're in a Cocoa app, the dispatch queue is drained by the main thread's NSRunLoop. So all we need to do is keep the run loop running while we're waiting for the test to finish. It looks like this:

- (void)testDoSomething {      __block BOOL hasCalledBack = NO;      void (^completionBlock)(void) = ^(void){                 NSLog(@"Completion Block!");         hasCalledBack = YES;     };       [MyObject doSomethingAsyncThenRunCompletionBlockOnMainQueue:completionBlock];      // Repeatedly process events in the run loop until we see the callback run.      // This code will wait for up to 10 seconds for something to come through     // on the main queue before it times out. If your tests need longer than     // that, bump up the time limit. Giving it a timeout like this means your     // tests won't hang indefinitely.       // -[NSRunLoop runMode:beforeDate:] always processes exactly one event or     // returns after timing out.       NSDate *loopUntil = [NSDate dateWithTimeIntervalSinceNow:10];     while (hasCalledBack == NO && [loopUntil timeIntervalSinceNow] > 0) {         [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode                                  beforeDate:loopUntil];     }      if (!hasCalledBack)     {         STFail(@"I know this will fail, thanks");     } } 
like image 194
BJ Homer Avatar answered Sep 22 '22 16:09

BJ Homer