注意您的Swift API(单元测试陷阱)中的协议扩展。

我们都喜欢协议扩展,它是Swift中面向协议编程(POP)最强大的元素之一。 尽管它们具有无可置疑的好处,但在少数情况下,您应该避免使用它们。 在本文中,让我演示一个潜在的陷阱,当您试图对依赖某些协议扩展功能的代码进行单元测试时,API使用者可能会陷入此陷阱。


快速提醒:方法分派

在Swift中,我们有三种方法分配: staticvtablemessage分配。 如果您不熟悉此术语,请允许我推荐Riazlab的一篇精彩文章。 简而言之,如果在多个位置(例如,父类或协议扩展)定义了相同的签名,则调度方法使用不同的技术来选择要执行的函数的具体实现。 例如,1)具有从 NSObject 继承的类 将始终使用消息调度,而2)值类型(结构,枚举)将始终使用静态调度。


好的,让我们回到危险的情况下,作为一个API创建者(我们都是API设计者,您还记得John Sundell的演讲吗?),我们遵循最佳实践并为我们的公共API提供协议抽象。 就本文而言,假设我们希望公开一个可能记录详细和错误消息的logger类,如下所示:

解决该方法的一种方法,以验证LogMock.log(_:message)是否已正确调用。 乍看起来似乎是一个合理的想法,但是这种方法存在一个固有的问题-在System类测试中,我们才刚刚开始测试由第三方开发人员编写的API中的verbose(message:)实现。 Logger.verbose(_:String)潜在的实现更改或错误可能会影响我们的测试结果。 顾名思义,单元测试应该在高度隔离的上下文中验证代码的单个单元(此处为System类)。

解决这个问题真的很简单。 作为API设计人员,您要做的就是将要在协议扩展中公开的所有函数/变量包含在主协议定义中,例如:

客户流

上面的修复程序专用于API创建者,但是即使您无权修改协议声明,也可以针对您的麻烦进行补救。 您将需要依赖自一个原始协议继承的自定义协议,并包括API设计器仅从协议扩展中公开的所有声明。 同时,在等待API设计者的修复程序时,您不再受到阻塞。

我们讨论了协议扩展,发现测试依赖协议扩展的代码可能很棘手。 幸运的是,有一个非常简单的补救方法–只需在协议定义中包含来自公共协议扩展的函数/变量定义即可。 因此,当您听到自己说:“让我们在协议扩展中实现它”时,您将知道该进行回退的时间了,并确保静态分发不会阻碍客户的单元测试。

尽管具有所有优点,但vtable分派的动态性质与静态 相比具有一定的性能开销,而静态表不必执行表查找。 但是,在大多数情况下,差异并不明显,因此这听起来是使API可测试的合理权衡。