Objective C - Unit testing dispatch_async block?
Asked Answered
F

3

15

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];
}
Flute answered 17/9, 2012 at 16:45 Comment(0)
O
41

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).

Omnivore answered 17/9, 2012 at 17:35 Comment(3)
Using this code, the block is not getting called at all, not even after the test is done.Flute
I've revised my answer. I tested this approach more thoroughly.Omnivore
Thanks. This method doesn't seem to be reliable, but I haven't found a better solution, so I'll go ahead and use it hoping that it won't break my tests.Flute
C
11

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

Chan answered 17/9, 2012 at 16:47 Comment(3)
Very interesting. Never heard of dispatch_group before. I might not use NSOperationQueues again. The only problem I had with GCD was lack of this functionalityFlute
@Flute Yup, dispatch groups are very useful, but rarely mentioned anywhere (even the official Apple docs aren't very verbose about them)Chan
This is very useful technique. It should receive more ups.Pye
P
1

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.

Pipeline answered 27/10, 2017 at 7:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.