32

这与我之前的问题有关,但不同的是,我想我会把它扔到一个新的问题中。我有一些代码在自定义队列上运行异步,然后在完成时在主线程上执行完成块。我想围绕这个方法编写单元测试。我的方法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 等的模式。有没有人有更好的模式来测试回调到主队列的异步代码?

4

6 回答 6

58

有两种方法可以将块分派到主队列以运行。第一个是 via dispatch_main,正如 Drewsmits 所提到的。dispatch_main但是,正如他还指出的那样,在您的测试中使用 using 存在一个大问题:它永远不会返回. 它只会坐在那里等待在永恒的剩余时间里运行任何出现的块。正如您想象的那样,这对单元测试没有太大帮助。

幸运的是,还有另一种选择。在手册页COMPATIBILITY的部分中,它是这样说的:dispatch_main

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");
    }
}
于 2011-10-20T00:21:13.847 回答
18

另一种方法,使用信号量和运行循环搅动。注意 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);
}
于 2011-12-13T23:59:48.690 回答
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");
}

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

于 2013-07-26T09:49:09.103 回答
7

Square 在他们的 SocketRocket 项目中巧妙地添加了 SenTestCase,使这变得简单。你可以这样称呼它:

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

该代码可在此处获得:

SenTestCase+SRTAdditions.h

SenTestCase+SRTAdditions.m

于 2012-12-02T23:07:13.567 回答
3

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

另一种方法是让您的单元测试在调度后进入其运行循环。然后完成块将有机会执行,您也有机会让运行循环超时,之后如果完成块没有运行,您可以认为测试失败。

于 2011-10-19T08:28:50.290 回答
2

基于这个问题的其他几个答案,我设置它是为了方便(和有趣):https ://github.com/kallewoof/UTAsync

希望它可以帮助某人。

于 2013-06-29T21:01:11.967 回答