Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Objective C - Unit testing dispatch_async block?

I read other posts that come up with solutions to this question. However, their solutions require hacky code to be added to my application in order to be able to test it. To me clean code is more important than unit test.

I use dispatch_async in my app regularly, and I'm having trouble unit testing it. The problem is the block executes after my test is already done, since it's running asynchronously on the main queue. Is there a way to somehow wait until the block is executed and then continue with the test.

I DON'T want to pass a completion to the block only because of unit-testing

- (viod)viewDidLoad
{
   [super viewDidLoad];

   // Test passes on this
   [self.serviceClient fetchDataForUserId:self.userId];


   // Test fails on this because it's asynchronous
   dispatch_async(dispatch_get_main_queue(), ^{
      [self.serviceClient fetchDataForUserId:self.userId];
   });
}

- (void)testShouldFetchUserDataUsingCorrectId
{
   static NSString *userId = @"sdfsdfsdfsdf";
   self.viewController.userId = userId;
   self.viewController.serviceClient = [[OCMockObject niceMockForClass:[ServiceClient class]];

   [[(OCMockObject *)self.viewController.serviceClient expect] fetchDataForUserId:userId];
   [self.viewController view]; 
   [(OCMockObject *)self.viewController.serviceClient verify];
}
like image 358
aryaxt Avatar asked Sep 17 '12 16:09

aryaxt


3 Answers

Run the main loop briefly to let it call the async block:

- (void)testShouldFetchUserDataUsingCorrectId {
   static NSString *userId = @"sdfsdfsdfsdf";
   self.viewController.userId = userId;
   self.viewController.serviceClient = [[OCMockObject niceMockForClass:[ServiceClient class]];

   [[(OCMockObject *)self.viewController.serviceClient expect] fetchDataForUserId:userId];
   [self.viewController view];
   [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]];
   [(OCMockObject *)self.viewController.serviceClient verify];
}

I suppose this could fail on a heavily-loaded system, or if you have a bunch of other stuff (timers or other blocks) going on the main thread. If that's the case, you need to run the run loop longer (which slows down your test case), or run it repeatedly until the mock object's expectations have been met or a timeout is reached (which requires adding a method to the mock object to query whether its expectations have been met).

like image 78
rob mayoff Avatar answered Nov 07 '22 11:11

rob mayoff


Wrap the execution into a dispatch_group and then wait for the group to finish executing all dispatched blocks via dispatch_group_wait().

like image 37
JustSid Avatar answered Nov 07 '22 10:11

JustSid


Make a dispatch_async wrapper with a similar method signature that in turn calls the real dispatch_async. Dependency inject the wrapper into your production class and use that.

Then make a mock wrapper, which records enqueued blocks and has an extra method for synchronously running all enqueued blocks. Maybe perform a recursive "blockception" if the blocks being executed in turn enqueued more blocks.

In your unit tests, inject the mock wrapper into the system under test. You can then make everything happen synchronously even though the SUT thinks it is doing async work.

dispatch_group sounds like a good solution as well, but would require your production class to "know" to ping the dispatch group at the end of the blocks it enqueues.

like image 44
tboyce12 Avatar answered Nov 07 '22 11:11

tboyce12