无需回调的OCMock for dispatch_async

问题描述

我的视图控制器上有一个使用dispatch_async的方法。一段时间后,它调用一个方法。在我的测试中,我想验证是否调用了后续方法

似乎大多数人对Ocmockdispatch_async的建议是使用XCTestExpectation并在任务完成时调用满载。但是,在我的测试中,由于该函数没有回调,因此我无法知道任务何时完成。结果是测试在任务完成之前已经完成,并且验证失败。

以下是一个可重现的最小示例,它演示了我的问题:

视图控制器

@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

测试

@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

正如您在测试中看到的那样,如果我在测试中添加了sleep命令,则代码可以正常工作。但是,我无法知道延迟将有多长,而且我也不想将其设置为安全的时间长度(如果实际上会更短)。我不想将延迟设置为5秒,如果有时只需要0.2秒。

有没有一种方法可以在发生usedInAsyncQueue时捕获呼叫,而不是等待一段时间然后再检查?

解决方法

也许最简单的情况如下-然后期望和实现都在同一条消息中。

@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

或者,类似这样,您可以在要测试的消息内完成。

@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

编辑4

这里是使用块参数的另一种选择,您的代码或多或少保持原样。

视图控制器

@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,^{
        [[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];
        });
    });
}

测试

@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];
}
,

技巧是将要验证的方法存根,并使其调用fulfill。在OP中的示例中,您将对方法usedInAsyncQueue进行存根并将其称为fulfill。然后,您可以等待它被调用,并在超时后失败测试。

这是完整的示例测试

- (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]);
}

我运行了该示例程序以及原始的生产代码,两者都运行良好