你如何测试异步方法?

时间:2009-05-04 03:12:34

标签: objective-c cocoa unit-testing

我有一个通过网络获取XML或JSON的对象。完成此提取后,它会调用一个选择器,传入返回的数据。所以,例如,我有类似的东西:

-(void)testResponseWas200
{
    [MyObject get:@"foo.xml" withTarget:self selector:@selector(dataFinishedLoading:)];  
}

我尝试了在Test类中实现dataFinishedLoading的路径,并尝试在该方法内部进行测试,但测试套件只是锁定。这似乎是嘲弄的一个案例,但我想知道其他人是否遇到过这种情况以及他们是如何处理的。

仅供参考:我正在使用gh-unit进行测试,任何以test *为前缀的方法都会自动执行。

4 个答案:

答案 0 :(得分:30)

我想到的三种方式是:NSRunLoop,信号量和群组。

NSRunLoop

__block bool finished = false;

// For testing purposes we create this asynchronous task 
// that starts after 3 seconds and takes 1 second to execute.
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0UL);
dispatch_time_t threeSeconds = dispatch_time(DISPATCH_TIME_NOW, 3LL * NSEC_PER_SEC);
dispatch_after(threeSeconds, queue, ^{ 
    sleep(1); // replace this with your task
    finished = true; 
});

// loop until the flag is set from inside the task
while (!finished) {
    // spend 1 second processing events on each loop
    NSDate *oneSecond = [NSDate dateWithTimeIntervalSinceNow:1];
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:oneSecond];
}

NSRunLoop是一个循环,它处理诸如网络端口,键盘或您插入的任何其他输入源之类的事件,并在处理这些事件后或在时间限制之后返回。当没有要处理的事件时,运行循环使线程进入休眠状态。所有Cocoa和Core Foundation应用程序都有一个运行循环。您可以在Apple的线程编程指南:Run Loops 或Mike Ash Friday Q&A 2010-01-01: NSRunLoop Internals 中阅读有关运行循环的更多信息。

在这个测试中,我只是使用NSRunLoop来休眠线程一秒钟。没有它,while中的常量循环将消耗100%的CPU核心。

如果块和布尔标志是在相同的词法范围内创建的(例如:两者都在方法内),那么该标志需要__block storage qualifier是可变的。如果该标志是一个全局变量,它就不需要它。

如果测试在设置标志之前崩溃,则线程将永远等待。添加时间限制以避免:

NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:2];
while (!finished && [timeout timeIntervalSinceNow]>0) {
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode 
                             beforeDate:[NSDate dateWithTimeIntervalSinceNow:1]];
}
if (!finished) NSLog(@"test failed with timeout");

如果您使用此代码进行单元测试,则插入超时的另一种方法是使用断言调度块:

// taken from https://github.com/JaviSoto/JSBarrierOperationQueue/blob/master/JSBarrierOperationQueueTests/JSBarrierOperationQueueTests.m#L118
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 2LL * NSEC_PER_SEC);
dispatch_after(timeout, dispatch_get_main_queue(), ^(void){
    STAssertTrue(done, @"Should have finished by now");
});

信号量

类似的想法,但睡眠直到信号量发生变化,或者直到时间限制:

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

// signal the semaphore after 3 seconds using a global queue
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0UL);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3LL*NSEC_PER_SEC), queue, ^{ 
    sleep(1);
    dispatch_semaphore_signal(semaphore);
});

// wait with a time limit of 5 seconds
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 5LL*NSEC_PER_SEC);
if (dispatch_semaphore_wait(semaphore, timeout)==0) {
    NSLog(@"success, semaphore signaled in time");
} else {
    NSLog(@"failure, semaphore didn't signal in time");
}

dispatch_release(semaphore);

如果我们永远等待dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);,我们就会被卡住,直到从任务中获取信号,该信号一直在后台队列上运行。

现在想象你必须等待几个街区。您可以使用int作为标志,或者创建以更高编号开头的信号量,或者您可以分组块并等到组完成。在这个例子中,我只使用一个块来执行后面的操作:

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0UL);

// dispatch work to the given group and queue
dispatch_group_async(group,queue,^{
    sleep(1); // replace this with your task
});

// wait two seconds for the group to finish
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 2LL*NSEC_PER_SEC);
if (dispatch_group_wait(group, timeout)==0) {
    NSLog(@"success, dispatch group completed in time");
} else {
    NSLog(@"failure, dispatch group did not complete in time");
}

dispatch_release(group);

如果出于某种原因(要清理资源?),您希望在群组完成后运行阻止,请使用dispatch_group_notify(group,queue, ^{/*...*/});

答案 1 :(得分:1)

异步回调通常需要运行消息循环。在测试代​​码中调用回调之后,停止消息循环是一种常见的模式。否则循环只是等待下一个任务,并且没有。

答案 2 :(得分:1)

@jano谢谢你我用你的帖子

制作了这个小工具

在PYTestsUtils.m

+ (void)waitForBOOL:(BOOL*)finished forSeconds:(int)seconds {
    NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:seconds];
    while (!*finished && [timeout timeIntervalSinceNow]>0) {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                                 beforeDate:[NSDate dateWithTimeIntervalSinceNow:1]];
    }
}

在我的测试文件中

- (void)testSynchronizeTime
{
    __block BOOL finished = NO;
    [self.connection synchronizeTimeWithSuccessHandler:^(NSTimeInterval serverTime) {
        NSLog(@"ServerTime %f", serverTime);
        finished = YES;
    } errorHandler:^(NSError *error) {
        STFail(@"Cannot get ServerTime %@", error);
        finished = YES;
    }];

    [PYTestsUtils waitForBOOL:&finished forSeconds:10];
    if (! finished)
        STFail(@"Cannot get ServerTime within 10 seconds");

}

变化

添加PYTestsUtils.m

+ (void)execute:(PYTestExecutionBlock)block ifNotTrue:(BOOL*)finished afterSeconds:(int)seconds {
    [self waitForBOOL:finished forSeconds:seconds];
    if (! *finished) block();
}

用法:

- (void)testSynchronizeTime
{
    __block BOOL finished = NO;
    [self.connection synchronizeTimeWithSuccessHandler:^(NSTimeInterval serverTime) {
        NSLog(@"ServerTime %f", serverTime);
        finished = YES;
    } errorHandler:^(NSError *error) {
        STFail(@"Cannot get ServerTime %@", error);
        finished = YES;
    }];

    [PYTestsUtils execute:^{
        STFail(@"Cannot get ServerTime within 10 seconds");
    } ifNotTrue:&finished afterSeconds:10];

}

答案 3 :(得分:0)

测试异步和多线程代码的最佳方法之一是使用事件记录。您的代码应该在有趣或有用的时间记录事件。通常,仅事件就足以证明逻辑正常工作。 Somtimes事件将需要有效载荷或其他元信息,以便它们可以配对或链接。

当运行时或操作系统支持高效且强大的事件机制时,这非常有用。这使您的产品能够附带“零售”版本的活动。在这种情况下,只有在需要调试问题或运行单元测试以证明这些问题正常时,才会启用事件。

使用零售(生产)代码中的事件可以在任何平台上进行测试和调试。这比调试或“检查”代码有很大的好处。

注意,就像断言一样,在放置事件时要小心 - 如果经常登录它们会很昂贵。但好消息是,现代操作系统和一些应用程序框架支持容易支持数十万个事件的事件机制。有些人支持对选定的事件进行堆栈跟踪。这可能非常强大,但通常要求在某个时间点可以使用符号 - 在记录时,或在目标系统上跟踪后处理时间。