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];
}
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).
Wrap the execution into a dispatch_group
and then wait for the group to finish executing all dispatched blocks via dispatch_group_wait()
.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With