Xcode 测试单独通过,当 运行 与其他测试一起通过时失败

Xcode tests pass in isolation, fail when run with other tests

我写了一些异步单元测试 XCTest 期望测试我写的网络 class。我的大部分测试每次都有效。

有一些测试在我 运行 整个套件时失败,但它们自己通过了。

其他测试失败,但使用相同的 URL 发出请求return 将适当的数据粘贴到浏览器中。

我的网络代码封装在 NSOperation 个对象中,这些对象在 NSOperationQueue 上 运行。 (我的操作队列是默认类型——我没有明确设置底层 GCD 队列是串行的还是并发的。)

我可以查看哪些内容来修复这些测试?阅读 this post on objc.io 后,我假设他们遇到了某种隔离问题。

你走在正确的道路上。 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();
  }];
}  

执行块将被设置,但您不会等待它完成,因为您实际上不是在等待内部块,而是等待外部块。关键是 XCTestWaits 实际上让测试完成,然后只检查承诺是否在某个时间段内实现,但与此同时它将开始 运行 其他测试。 success() 可以出现在任意数量的地方并产生任意数量的奇怪行为。

隔离行为(相对于无隔离)来自这样一个事实,即如果你 运行 只有这个测试,一切可能都很好,因为运气好,但如果你 运行 多次测试,CoreData 块可能只是一直停留到下一个异步测试,然后 "unblock" 它的执行,它将在未来某个随机时间开始执行,以进行一些随机的未来测试。

短期明确的破解方法是暂停您的测试,直到事情完成。这是一个例子:

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

这明确地阻止了一个测试在一切都完成之前完成,这意味着在这个测试完成之前没有其他测试可以运行。

如果你的 tests/code 中有很多这样的情况,我会推荐 objc.io 创建一个你可以在每次测试后等待的调度组的解决方案。

在与 NSOperationQueue 和来自 waitUntilAllOperationsAreFinished 的看似不正确的 return 战斗了几天之后,我想到了一个更简单的选择:将测试划分为多个测试目标。这为您的测试提供了自己的 'app' 环境,更重要的是,在这种情况下,确保 Xcode/XCUnit 将按顺序 运行 它们,以便它们不会相互干扰 - 除非它们做一些事情,例如离开数据库变脏(无论如何都应该失败)。

最快的方法是复制您的测试目标,从原始目标中删除失败的测试,并从新目标中删除失败测试以外的所有内容。请注意,如果您有多个测试相互干扰,您可能需要多个目标才能实现足够的隔离。

您可以通过检查测试目标方案来检查测试是否已执行。在该方案中,您应该看到两个(全部)测试目标以及在它们下面的个人测试。

我在 运行 单元测试时遇到了这个问题,当时 Publisher 正在直播 API(我稍后会嘲笑)速度太快并且速率受限。这是我想出的解决方法。

  1. 在测试class中,我声明了一个DispatchQueue属性,像这样:
    let testQueue = DispatchQueue(label: "Test Queue", qos: .default)
    let delay = DispatchQueue.SchedulerTimeType.Stride(1.0)

  1. 然后,在每个测试方法中,我在发布者声明之后添加了以下内容:
.delay(for: delay, scheduler: testQueue)

我最终玩了延迟直到测试通过。