Code Review

断言

xUnit
t.FatalfpanicisNotNilstringEq

综上,上面那个例子应该改写为:

使用可读性高的子测试用例名

t.Run
t.Log

直接比较结构体

如果函数返回的是结构体,不推荐一个一个字段的比较,而是构造出你预期的结果,用下文提到的直接比较。该规则同样适用于数组和字典。

io.Reader

如果函数返回多个结果,逐个比较并打印,不必拼成一个结构体。

只比较稳定的结果

如果被测函数依赖的外部包不受控制,导致输出结果不稳定,就该避免在测试中使用该结果。相反,应该去比较那些在语义上稳定的信息。

json.Marshaljson

相等性比较和diff

==

使用 cmp 包的 cmp.Equal 可直接比较两个任意的对象,使用 cmp.Diff 则会输出这两个对象间的差异,而且可读性非常高。

cmp
reflect.DeepEqualcmpreflect.DeepEqual
cmpcmp.Compare(proto.Equal)

不仅打印期望值,也要打印实际值

YourFunc(%v) = %v, want %v

对于 diff 的输出,期望结果和实际结果谁前谁后不明显,这时需要加入额外信息帮助理解。这两个什么顺序并不重要,重要的是整个工程应该具有一致性。

标识函数名

在大部分测试中,失败消息应该包含所在函数名,即使该消息显而易见来自测试函数。

优先使用:

而不是:

标识输入

在大部分测试中,函数输入参数也应该包含在失败消息中。如果输入参数的相关属性不明显(比如,参数较大或晦涩难懂),你应该在测试名中描述本测试的内容,并且将描述信息放入错误消息中。

对于表格驱动型测试,不要将序号作为测试名的一部分。在测试用例失败后,没人希望回到表格中一个个数来找出失败来自哪个用例。

失败继续执行

即使测试用例遇到了失败,它也应尽可能地继续执行,以便能在一次运行中打印出所有失败检查点。这样,如果有人要依照测试结果修复代码时,不用一遍遍重复执行用例来找出下一个 bug。

t.Errort.Fatalt.Error
t.Fatalt.Fatal
t.Runt.Errorconitnuet.Runt.Fatal

标记测试辅助函数

辅助函数常用于 setup 和 teardown 任务中,比如构造一个测试数据。

在辅助函数中调用 t.Helper 后,如果辅助函数中某个判断出错,那在测试日志中的错误提示会忽略该辅助函数的调用栈,标记出错的行会焦点在测试用例中,而非在辅助函数中。有点绕,看个例子便一目了然。

t.Helper

出错信息为:

标记代码后:

出错信息为:

testHelper

打印 diff

如果函数返回的输出比较长,而出错的地方只是其中一小段,那很难一眼看出区别。这对调试不友好,建议直接输出期望和实际结果的 diff 值。

表格驱动测试 vs 多个测试函数

当多个测试用例有着相同的测试逻辑,只是输入数据不同时,就应该使用 表格驱动测试 方法。

而当每个测试用例需用不同的方法验证时,表格驱动就显得不合适,因为那样就不得不写一堆控制变量放入表格中,将原本的测试逻辑淹没其中,降低了用例的可读性和表格的可维护性。

实际测试两种方法需结合使用。比如可以写两个表格驱动测试方法,一个测试函数的正常返回结果,另一个测试不同错误消息。

测试错误语义

reflect.DeepEqual

而依赖库中的错误消息则相对稳定,拿来做字符串比较是可接受的。

fmt.Errorf

许多人并不关心他们的 API 返回具体什么错误消息,这种情况下,单元测试中只做错误非空判断就可以了。