在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
类型上的==
函数。 在不给您带来全部数学负担的情况下,平等是“等价关系”的一个示例,而关于等价关系的重要内容之一就是对称性的概念。 简而言之,如果我有两个某种类型的实例a
和b
,则永远不会出现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())
哦! ✨