你走在正确的道路上。 objc.io 文章建议的解决方案可能是正确的方法,但确实需要一些重构。如果您想在进行代码更改狂潮之前将测试作为第一步,那么您可以这样做。
通常,您可以使用 XCTestExpectations 进行几乎所有的异步测试。标准模式可能是这样的:
XCTestExpectation *doThingPromise = [self expetationWithDescription:@"Bazingo"];
[SomeService doThingOnSucceed:^{
[doThingPromise fulfill];
} onFail:^ {
}];
[self waitForExpectationsWithTimeout:1.0 handler:^(NSError *error) {
expect(error).to.beNil();
}]
如果 [SomeService doThingOnSucceed:onFail:] 触发异步请求然后直接解析,则此方法可以正常工作。但如果它做一些更奇特的事情会怎样:
+ (void)doThingOnSucceed:onFail: {
[Thing do it];
[self.context performBlock:^{
// Uh oh Farfalle-Os
success();
}];
}
perform 块将被设置,但您不会等待它完成,因为您实际上并没有在内部块上等待,而只是在外部块上。关键是 XCTestWaits 实际上让测试完成,然后只检查承诺是否在一段时间内实现,但同时它将开始运行其他测试。 success() 可以出现在任意数量的位置并产生任意数量的怪异行为。
隔离行为(相对于不隔离)来自这样一个事实,即如果您只运行此测试,由于运气,一切可能都很好,但如果您运行多个测试,CoreData 块可能会一直停留,直到下一个测试async,然后将“解锁”其执行,并将在未来某个随机时间开始执行,以进行一些随机的未来测试。
短期明确的破解方法是暂停您的测试,直到事情完成。这是一个例子:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[SomeService doThingOnComplete:^{
dispatch_semaphore_signal(semaphore);
}];
while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]];
}
这会明确阻止测试在所有内容完成之前完成,这意味着在此测试完成之前无法运行其他测试。
如果您的测试/代码中有很多此类情况,我会推荐 objc.io 解决方案,即创建一个调度组,您可以在每次测试后等待。