单元测试异步队列的模式,在完成时调用主队列

时间:2011-10-19 06:55:16

标签: objective-c grand-central-dispatch sentestingkit

这与我之前的question有关,但又不同,我想我会把它扔进一个新的。我有一些代码在自定义队列上运行异步,然后在完成时在主线程上执行完成块。我想围绕这种方法编写单元测试。 MyObject上的方法看起来像这样。

+ (void)doSomethingAsyncThenRunCompletionBlockOnMainQueue:(void (^)())completionBlock {

    dispatch_queue_t customQueue = dispatch_queue_create("com.myObject.myCustomQueue", 0);

    dispatch_async(customQueue, ^(void) {

        dispatch_queue_t currentQueue = dispatch_get_current_queue();
        dispatch_queue_t mainQueue = dispatch_get_main_queue();

        if (currentQueue == mainQueue) {
            NSLog(@"already on main thread");
            completionBlock();
        } else {
            dispatch_async(mainQueue, ^(void) {
                NSLog(@"NOT already on main thread");
                completionBlock();
        }); 
    }
});

}

我投入了主队列测试以获得额外的安全性,但它总是击中dispatch_async。我的单元测试如下所示。

- (void)testDoSomething {

    dispatch_semaphore_t sema = dispatch_semaphore_create(0);

    void (^completionBlock)(void) = ^(void){        
        NSLog(@"Completion Block!");
        dispatch_semaphore_signal(sema);
    }; 

    [MyObject doSomethingAsyncThenRunCompletionBlockOnMainQueue:completionBlock];

    // Wait for async code to finish
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
    dispatch_release(sema);

    STFail(@"I know this will fail, thanks");
}

我创建了一个信号量,以阻止测试在异步代码之前完成。如果我不要求完成块在主线程上运行,这将很有用。然而,正如一些人在我上面链接的问题中指出的那样,测试在主线程上运行然后我在主线程上排队完成块的事实意味着我将永远挂起。

从异步队列调用主队列是我看到很多用于更新UI等的模式。有没有人有更好的模式来测试回调到主队列的异步代码?

6 个答案:

答案 0 :(得分:58)

有两种方法可以将分派到主队列的块运行。第一个是通过dispatch_main,正如Drewsmits所提到的那样。但是,正如他所指出的,在测试中使用dispatch_main存在一个很大的问题:它永远不会返回。它只会坐在那里等待运行永恒的剩余部分。对于单元测试来说,这没有那么有用,正如你想象的那样。

幸运的是,还有另一种选择。在dispatch_main man pageCOMPATIBILITY部分,它说明了这一点:

  

Cocoa应用程序不需要调用dispatch_main()。提交的块   主队列将作为“共同模式”的一部分执行   应用程序的主要NSRunLoop或CFRunLoop。

换句话说,如果你在Cocoa应用程序中,调度队列将被主线程的NSRunLoop耗尽。因此,我们需要做的就是在等待测试完成时保持运行循环运行。它看起来像这样:

- (void)testDoSomething {

    __block BOOL hasCalledBack = NO;

    void (^completionBlock)(void) = ^(void){        
        NSLog(@"Completion Block!");
        hasCalledBack = YES;
    }; 

    [MyObject doSomethingAsyncThenRunCompletionBlockOnMainQueue:completionBlock];

    // Repeatedly process events in the run loop until we see the callback run.

    // This code will wait for up to 10 seconds for something to come through
    // on the main queue before it times out. If your tests need longer than
    // that, bump up the time limit. Giving it a timeout like this means your
    // tests won't hang indefinitely. 

    // -[NSRunLoop runMode:beforeDate:] always processes exactly one event or
    // returns after timing out. 

    NSDate *loopUntil = [NSDate dateWithTimeIntervalSinceNow:10];
    while (hasCalledBack == NO && [loopUntil timeIntervalSinceNow] > 0) {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                                 beforeDate:loopUntil];
    }

    if (!hasCalledBack)
    {
        STFail(@"I know this will fail, thanks");
    }
}

答案 1 :(得分:18)

另一种方法,使用信号量和runloop搅动。请注意,如果dispatch_semaphore_wait超时,则返回非零值。

- (void)testFetchSources
{
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    [MyObject doSomethingAsynchronousWhenDone:^(BOOL success) {
        STAssertTrue(success, @"Failed to do the thing!");
        dispatch_semaphore_signal(semaphore);
    }];

    while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW))
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                                 beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]];

    dispatch_release(semaphore);
}

答案 2 :(得分:7)

Square在他们的SocketRocket项目中包含了对SenTestCase的一个聪明的补充,这使得这很容易。你可以这样称呼它:

[self runCurrentRunLoopUntilTestPasses:^BOOL{
    return [someOperation isDone];
} timeout: 60 * 60];

代码可在此处获取:

SenTestCase+SRTAdditions.h

SenTestCase+SRTAdditions.m

答案 3 :(得分:7)

BJ Homer的解决方案是迄今为止最好的解决方案。我已经创建了一些基于该解决方案的宏。

在这里查看项目https://github.com/hfossli/AGAsyncTestHelper

- (void)testDoSomething {

    __block BOOL somethingIsDone = NO;

    void (^completionBlock)(void) = ^(void){        
        NSLog(@"Completion Block!");
        somethingIsDone = YES;
    }; 

    [MyObject doSomethingAsyncThenRunCompletionBlockOnMainQueue:completionBlock];

    WAIT_WHILE(!somethingIsDone, 1.0); 
    NSLog(@"This won't be reached until async job is done");
}

WAIT_WHILE(expressionIsTrue, seconds) - 宏将评估输入,直到表达式不为真或达到时间限制。我认为很难让它比这更清洁

答案 4 :(得分:3)

在主队列上执行块的最简单方法是从主线程调用dispatch_main()。但是,据我所知,从文档中可以看出,永远不会返回,所以你永远无法判断你的测试是否失败。

另一种方法是在发送后让您的单元测试进入run loop。然后,完成块将有机会执行,并且您还有机会使运行循环超时,之后如果完成块未运行,您可以认为测试失败。

答案 5 :(得分:2)

根据这个问题的其他几个答案,我为了方便(和乐趣)而进行了设置:https://github.com/kallewoof/UTAsync

希望它有所帮助。