dispatch_async 的 OCMock 无回调

OCMock for dispatch_async without callback

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

似乎大多数人对处理 OCMock 和 dispatch_async 的建议是使用 XCTestExpectation 并在任务完成时调用 fulfill。但是,在我的测试中,我无法知道任务何时完成,因为该函数没有回调。结果是测试在任务完成之前完成,验证失败。

这里有一个最小的可重现示例来说明我的问题:

视图控制器

@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

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

有没有办法在 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, 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];
        });
    });
}

测试

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

我运行这个示例程序以及我的原始生产代码,两者都能完美运行