OCMock for dispatch_async without callback

479 Views Asked by At

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?

2

There are 2 best solutions below

0
Malachi Holden On BEST ANSWER

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

15
skaak On

Maybe simplest is as below - then the expectation and its fulfilment are all in the same message.

@implementation Test

- ( void ) testSomeFunc
{
    XCTestExpectation * expectation = [[XCTestExpectation alloc] initWithDescription:@"Test"];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ {
        // Some long task

        // Now done ...
        dispatch_async(dispatch_get_main_queue(), ^{

            // Note this fulfill happens in the test code
            [expectation fulfill];

            // This calls the original code, not the test code
            [self usedInAsyncQueue];
        });
    });

   // Wait for it ...
   [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:10];
}

@end

Alternatively, something like this, where you fulfill inside the message you want to test.

@interface Test
@property (nonatomic,strong) XCTextExpectation * expectation;
@end

@implementation Test

- ( void ) testSomeFunc
{
  // Here you need some reference to the expectation as it is fulfilled inside the func itself later
  self.expectation = [[XCTestExpectation alloc] initWithDescription:@"Test"];

  // Some code that eventually calls testFunc async and in a block
  // Note this calls *test*Func, it calls test code
  ...

  // Wait for it as before
}

// This is a test func
- ( void ) testFunc
{
  // Here the fulfill takes place inside test
  [self.expectation fulfill];
  // rest of func
  // here you call the code itself, not the test...
}

@end

EDIT 4

Here is another alternative using block parameters and your code more or less as is.

View Controller

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
}

// Using a completion block here specifically for testing
// Call this with nil when not testing
- (void)usesAsyncQueue:( void (^)( void ) ) completion {
    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(), ^{
            if ( completion )
            {
               completion();
            }
            [self usedInAsyncQueue];
        });
    });
}

Test

@implementation ViewControllerTest

- (void)testUsesAsyncQueue {
    ViewController * testViewController = [[ViewController alloc] init];
    id viewControllerMock = OCMPartialMock(testViewController);
    
    XCTestExpectation * expectation = [[XCTestExpectation alloc] initWithDescription:@"Test"];

    // Here we pass a block that will fulfill the expectation    
    [testViewController usesAsyncQueue:^ { expectation.fulfill; }];

    // Wait for it ...
   [self waitForExpectations:[NSArray arrayWithObject:expectation] timeout:10];
}