Objective c 如何测试异步方法?

Objective c 如何测试异步方法?,objective-c,cocoa,unit-testing,Objective C,Cocoa,Unit Testing,我有一个通过网络获取XML或JSON的对象。完成此提取后,它将调用选择器,传入返回的数据。举个例子,我想说: -(void)testResponseWas200 { [MyObject get:@"foo.xml" withTarget:self selector:@selector(dataFinishedLoading:)]; } 我尝试了在测试类中实现dataFinishedLoading并尝试在该方法中进行测试的方法,但测试套件只是锁定了。这似乎是嘲笑的理由,但我想知道其他

我有一个通过网络获取XML或JSON的对象。完成此提取后,它将调用选择器,传入返回的数据。举个例子,我想说:

-(void)testResponseWas200
{
    [MyObject get:@"foo.xml" withTarget:self selector:@selector(dataFinishedLoading:)];  
}
我尝试了在测试类中实现dataFinishedLoading并尝试在该方法中进行测试的方法,但测试套件只是锁定了。这似乎是嘲笑的理由,但我想知道其他人是否遇到过这种情况,以及他们是如何处理的


仅供参考:我正在使用gh单元进行测试,任何带有test*前缀的方法都会自动执行。

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

当运行时或操作系统支持高效、健壮的事件机制时,这一点最为有用。这使您的产品能够在“零售”版本中随事件一起提供。在此场景中,只有当您需要调试问题或运行单元测试以证明thins正常工作时,才会启用事件

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


注意,与断言一样,要小心放置事件的位置-如果经常登录,它们可能会很昂贵。但好消息是,现代操作系统和一些应用程序框架支持事件机制,可以轻松支持成千上万个事件。有些支持对所选事件进行堆栈跟踪。这可能非常强大,但通常要求符号在某个时间点可用—无论是在日志记录时,还是在目标系统上跟踪后处理时间

异步回调通常需要消息循环才能运行。在测试代码中调用回调后停止消息循环是一种常见的模式。否则,循环只会等待下一个任务,而不会有任何任务。

想到的三种方法是:nsrunlop、信号量和组

NSRunLoop nsrunlop是一个循环,用于处理网络端口、键盘或您插入的任何其他输入源等事件,并在处理这些事件后或在一个时间限制后返回。当没有要处理的事件时,运行循环将线程置于睡眠状态。所有可可和核心基础应用都有一个运行循环。您可以在Apple的线程编程指南中或Mike Ash中阅读有关运行循环的更多信息

在本测试中,我只是使用nsrunlop使线程休眠一秒钟。如果没有它,
while
中的持续循环将消耗100%的CPU核心

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

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

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\u semaphore\u wait(semaphore,dispatch\u TIME\u 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\u group\u notify(group,queue,^{/*…*/})

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

}

您可以使用nsrunlop执行此操作,在测试方法中显式运行循环。当然,只有当异步方法实际使用run循环时,但Cocoa的所有内置异步方法都是如此。所有优秀和常见的方法都是如此。OP可能对以下测试异步方法和将异步方法转换为同步方法的示例感兴趣:(具体请参见testAsync),以及,请参见synchronousResultForCryptor:data:error:。@RobNapier我正在查看您的testAsync--我认为您需要在循环条件中添加[timeout TimeIntervalenceNow]>0,否则它就不起超时的作用。回答得很好。可能值得一提-如果任务依赖计时器,则可能需要确保当前运行循环在正确的时间运行。实际上与块无关,但我经常将两者结合使用。
- (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];

}