没有人期望完成调查!

如何测试非调用完成处理程序方案

使用XCTestExpectation测试完成处理程序API幸福路径场景非常简单。 如果您的测试用例必须验证给定的处理程序是否未被调用,事情就会变得复杂。 如果在您模拟的其他事件之后同步调用它,则仍然很简单。 但是,如果延迟执行时间呢? 在本文中,我将向您演示一种解决方案,以100%确保不会调用给定的完成处理程序-快速可靠。


我们来谈谈提供完成处理程序的一些异步任务的单元测试,比如说从服务器下载JSON。 似乎很明显,我们必须验证在快乐路径方案中是否调用了完成。 XCTest提供了一个不错的API:只需创建一个期望,在完成时实现它,然后在测试结束时等待所有实现:

天真的解决方案

另一种方法是稍等片刻,并假设只要未完成完成(例如0.1s),它就永远不会执行。 它有两个明显的缺点:

  • 您必须猜测要等待多长时间。 是0.1s还是0.01s? 如果设置不正确,很容易导致测试无效或不稳定。
  • 它增加了单个测试的持续时间,默认情况下默认应尽可能快。

这两个缺点都可能导致危险的陷阱:

当测试花费的时间太长或不稳定时,您的队友(或将来的您)就会开始忽略它们,而迟早就放弃它们。

让我问一个问题:您何时能100%确定系统的某些外部部分永远不会调用完成? 答: 在没有人保留的情况下,不再有完成任务!

为了验证没有什么延迟执行块,我们必须确保所有其他实例都丢失对完成的引用。 好的,听起来很简单!

但是如何实施呢? 当然,我们不能定义对块的弱引用: 'weak' may only be applied to class and class-bound protocol types 。 因此,让我们定义简单的类ReferenceObserver ,它仅通知其deinit

目前,我们对ReferenceObserver实例有两个引用: referencecapturedReference (已完成)。 注意:我们必须将虚拟分配添加到匿名变量 _ ,否则Swift根本不会捕获它。

如您所料,让我们摆脱助手参考(只是调用reference = nil )和voilà之一:

只有完成处理程序块会保留对ReferenceObserver并且一旦从内存中删除了完成处理程序, deinitCompletion自动满足deinitCompletion期望:

这是我们的最终测试:

我们最终得到了一个解决方案,该解决方案是:

  • 不需要增加测试持续时间,因为在ReferenceObserver deinit之后, waitForExpectations立即退出。
  • 确保不会调用完成。

您可能想知道, 在测试结束时 0.1s 什么 意思? 这只是 Downloader 实例清除其处理程序引用 最大 间隔

替代符号

如果您像我一样,并且甚至在单元测试中都不喜欢代码库中的可选参数,则可以利用其他范围来摆脱ReferenceObserver? 可选类型。 您知道吗,您可以创建 do 部分 ,而不会产生 任何 catch ,它只会创建一个额外的作用域? 如果我们在专用范围内初始化ReferenceObserver ,则其引用将在其末尾删除,因此我们可以删除可选性。 一旦//Arrange节结束,对观察者的唯一引用将附加到downloader 。 下面的符号等效于先前的代码:

我们在这里要付出的代价是可读性。 一些不熟悉空范围作用域构造的开发人员可能不了解在这里实际发生什么以及为什么发生。 因此,尽管我总体上不愿选择一个可选项,但我还是会使用ReferenceObserver?选择以前的符号ReferenceObserver? 由于其可读性和清晰度。
你对这个有什么看法? 你是否同意我的观点? 您可以在下面发表评论,或者在Twitter上关注我,@ norapsi。


边注
最后,让我在这里提醒有关编写单元测试的另一个黄金法则:

顺便说一句。 这种修改后的测试称为 变异测试

这对于上述解决方案尤其重要。 请记住,如果您没有在一个块中正确捕获ReferenceObserver引用,则测试可能会假阳性成功。
如果您真的很害怕,则可以在ReferenceObserver实现中添加例如额外的防护,并通过使用do{}引入额外的作用域, deinit仅在测试或管理器参考生命周期中的明确点之后通知有关deinit 。 我宁愿建议坚持使用良好的做法,而不要添加额外的代码。 如果您有兴趣,可以在此要点中使用安全防护装置进行样品测试。


提出的解决方案非常适合验证快速,可靠的“未调用”方案。 您可能还会发现检查某些内容是否仍然保留对块的引用(无原因)并可能导致内存泄漏很有用。

最后,在实现ReferenceObserver所需的几行代码中,您最终可以确保不会在没有任何延迟的情况下调用给定的块。 因此,现在您可以将此技术添加到开发人员工具箱中,并保护自己免受意外的“完成询问”的影响。