iosobjective-cocmockdispatch-async

OCMock for dispatch_async without callback


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?


Solution

  • 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