在Swift中编写更好的单元测试:第二部分

问题

在本系列的第一部分中,我定义了我认为是“良好”的单元测试。 这是我最终得到的定义:

如果我们可以同意一个单元测试(或者实际上是任何一个测试)是由一些设置 ,我们正在测试的动作以及关于该动作效果的断言组成的,那么我可以这么简单地说,一种“好的”单元测试可以使这三个组件中的每一个都清晰可见。

您可能还记得,我们以如下所示的测试结束了这篇文章:

  var sut,其他:用户! 
  func test_equals_allPropertiesMatch_isTrue(){ 
(sut,other)=(.create(),.create())
XCTAssertEqual(ut,其他)
XCTAssertEqual(other,sut)
}
  func test_equals_nameDiffers_isFalse(){ 
(其他)=(.create(name:“ Jo”),.create())
XCTAssertNotEqual(sut,其他)
XCTAssertNotEqual(other,sut)
}

我们压缩了设置,以便测试主体仅包含对测试场景重要的信息。 例如,当重要的是两个用户对象的名称不同时,我们在设置中仅包括该信息。

这是提高测试总体可读性的好方法。 但是,我们还有更多可以做的事情。

您可能查看了上述测试,并认为“设置还可以,但是为什么在这里却有两个断言呢?”很好的问题! 我很高兴您关注这个问题。

请记住,以上测试涵盖了User类型上的==函数。 在不给您带来全部数学负担的情况下,平等是“等价关系”的一个示例,而关于等价关系的重要内容之一就是对称性的概念。 简而言之,如果我有两个某种类型的实例ab ,则永远不会出现a == b为true但b == a为false的情况。 如果这是可能的,那么我们对平等的定义是有缺陷的。 因此,有必要验证我们的==的自定义定义是对称的。

但是,仅看那里的两个断言,就完全不清楚这是意图。 这些测试只是在断言一件事。 经过一段时间和倾向,其他工程师可能会弄清楚为什么我们添加第二个断言,但是我们绝对可以做得更好。

但是…如何?

解决方案

让我们开始做我们可能想到的最简单的事情。 我们有两行代码,而我们只想有一行代码。 解决方案? 一个功能!

  func assertSymmetricallyEqual(_ sut:用户,_其他:用户){ 
XCTAssertEqual(ut,其他)
XCTAssertEqual(other,sut)
}

(注意:这里我们专注于测试是否相等,但是对于不平等,您可以做完全相同的事情)。

现在我们的测试变为:

  var sut,其他:用户! 
  func test_equals_allPropertiesMatch_isTrue(){ 
(sut,other)=(.create(),.create())
assertSymmetricallyEqual(sut,other)
}

如果我们运行此测试并通过测试,则一切正常。 但是,当断言失败时,Xcode将失败显示在错误的位置。

显然,这是不可接受的。 如果我们试图快速诊断失败的测试,我们希望能够准确查看失败的原因和位置。 更重要的是,如果两个或三个测试都使用相同的assert函数都失败了,那么我们将很难将失败分开。

幸运的是, XCTAssertEqual文档提供了一个解决方案。 在那里,我们可以看到该函数的Swift声明,如下所示:

 func XCTAssertEqual( 
_ expression1: @autoclosure () throws -> T,
_ expression2: @autoclosure () throws -> T,
_ message: @autoclosure () -> String = default,
file: StaticString = #file,
line: UInt = #line
) where T : Equatable

我已经将两条最重要的行加粗了。

由于XCTAssertEqual是Swift中的一个函数(而不是Objective-C中的宏),因此有一个小技巧可以使Xcode在正确的位置呈现故障。 当我们调用XCTAssertEqual ,Swift编译器会将调用它的文件( #file )和调用它的行( #line )添加为默认参数。

由于我们不希望看到故障继续发生,因此我们对XCTAssertEqual的调用不会发生,因此我们需要付出一些额外的努力才能使一切正常运行。 本质上,我们需要告诉XCTAssertEqual正确的文件和行以显示失败。 这应该是调用我们的自定义assert方法的#file#line 。 因此,按照Apple文档中的示例,我们得出以下结论:

  func assertSymmetricallyEqual( 
_ sut:用户,_其他:用户,
文件:StaticString = #file,
行:UInt = #line
){
XCTAssertEqual(sut,other, file:file,line:line
XCTAssertEqual(other,sut, file:file,line:line
}

方法的更改已用粗体标记。

再次运行测试,我们得到以下结果:

好的! 这正是我们想要的。 现在,这些故障正好显示在我们需要查看它们的位置。 🙌

重构

上面所做的事情很好,但是一旦我们要断言一种以上的类型满足对称相等性,我们将以一张通往复制镇的单程票找到自己。 🏘🏘🏘🏘

解决方案? 为此,我们可以参考之前的文档。 Swift的XCTAssertEqual方法在某些T: Equatable是通用的-因此,让我们对assertSymmetricallyEqual

  func assertSymmetricallyEqual 
_ sut: T ,_其他: T
文件:StaticString = #file,
行:UInt = #line
){
XCTAssertEqual(sut,other,file:file,line:line)
XCTAssertEqual(other,sut,file:file,line:line)
}

更改再次以粗体标记。 看看那个! 几乎没有!

现在,我们有了一个不错的,可重用的自定义断言,该断言确认一个Equatable类型的两个实例相等。 多么活着的时间!

红利

在Clue,我们实际上已将这种模式进一步发展了一步。 我们有一个内部框架,可用于跨不同模块的测试助手。 该框架包含以下struct

 公共结构Assert  { 
私人出租对象:T?

init(_ subject:T?){
self.subject =主题
}
}

“好的,太好了,”我听到你哭了,“但这绝对没有任何作用。”而且你是对的! 因为真正的魔力发生在这种类型的扩展中:

 扩展名断言T:等于{ 
公共函数对称等于
其他:T,
文件:StaticString = #file,
行:UInt = #line
){
XCTAssertEqual(主题,其他,文件:file,行:line)
XCTAssertEqual(其他,主题,文件:file,行:line)
}
}

当我们需要调用此方法时,它看起来像这样:

 断言(sut).symmetrically等于(其他) 

美丽!

并且,作为一种特殊待遇,这是一个示例,说明如何将这种模式扩展到其他类型的断言。 这是我个人最喜欢的自定义断言函数,可让我们断言已抛出特定错误:

 扩展名断言T:错误和相等{ 
公共函数isThrownIn(
_表达式:@autoclosure()引发->(),
文件:StaticString = #file,
行:UInt = #line
){
XCTAssertThrowsError(
尝试expression(),文件:文件,行:行
){
XCTAssertEqual(
主题,$ 0为? T,
文件:文件,行:行

}
}
}

(我不得不将缩进程度提高到荒谬的程度,以使其适合中等代码块。很抱歉!)

然后,我们可以如下使用它:

 断言(Errors.someError).isThrownIn(尝试someMethodThatThrows()) 

哦! ✨