I have a method on my view controller that uses dispatch_async. After some time, it calls another method. In my test, I want verify that the followup method gets called.
It appears that most people's advice for dealing with OCMock and dispatch_async is to use XCTestExpectation and call fulfill when the task is complete. However, in my test I have no way of knowing when the task is complete, since the function doesn't have a callback. The result is that the test completes before the task completes, and the verify fails.
Here is a minimal reproducible example that demonstrates my issue:
View Controller
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)usesAsyncQueue{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5.0]]; //long running task, e.g. network request
dispatch_async(dispatch_get_main_queue(), ^{
[self usedInAsyncQueue];
});
});
}
- (void)usedInAsyncQueue{
// we want to verify that this is called
}
@end
Test
@implementation ViewControllerTest
- (void)testUsesAsyncQueue {
ViewController * testViewController = [[ViewController alloc] init];
id viewControllerMock = OCMPartialMock(testViewController);
OCMExpect([viewControllerMock usedInAsyncQueue]);
[testViewController usesAsyncQueue];
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5.1]]; //comment this out and the test fails
OCMVerify([viewControllerMock usedInAsyncQueue]);
}
@end
As you can see in my test, if I add a sleep command in the test the code works fine. However, I have no way of knowing how long the delay will be, and I don't want to set it to a safe length of time if in reality it would be shorter. I don't want to set the delay to 5 seconds if sometimes it would only take 0.2 seconds.
Is there a way of catching the call to usedInAsyncQueue
when it happens instead of waiting a while and then checking?
The trick is to stub the method you want to verify, and make it call fulfill
. In the example in the OP, you would stub the method usedInAsyncQueue
and make it call fulfill
. Then you can wait for it to be called and fail the test on a timeout.
Here is the full example test
- (void)testUsesAsyncQueue {
ViewController * testViewController = [[ViewController alloc] init];
id viewControllerMock = OCMPartialMock(testViewController);
XCTestExpectation * expectation = [[XCTestExpectation alloc] initWithDescription:@"test"];
OCMStub([viewControllerMock usedInAsyncQueue]).andDo(^(NSInvocation* invocation){
[expectation fulfill];
});
OCMExpect([viewControllerMock usedInAsyncQueue]);
[testViewController usesAsyncQueue];
[self waitForExpectations:@[expectation] timeout:5.1];
OCMVerify([viewControllerMock usedInAsyncQueue]);
}
I ran this example program as well as my original production code, and both work perfectly