Go Wiki: Go 测试注释
此页面是对Go 代码审查注释的补充,但专门针对测试代码。
在编辑此页面之前,请先讨论更改,即使是小的更改。很多人都有自己的看法,这里不适合进行编辑战。
- 断言库
- 选择易于人类阅读的子测试名称
- 比较稳定结果
- 比较完整结构
- 相等比较和差异
- 先 Got 后 Want
- 标识函数
- 标识输入
- 继续进行
- 标记测试助手
- 打印差异
- 表驱动测试与多个测试函数
- 测试错误语义
断言库
避免使用“assert”库来辅助你的测试。来自 xUnit 框架的 Go 开发者经常想写如下代码:
assert.IsNotNil(t, "obj", obj)
assert.StringEq(t, "obj.Type", obj.Type, "blogPost")
assert.IntEq(t, "obj.Comments", obj.Comments, 2)
assert.StringNotEq(t, "obj.Body", obj.Body, "")
但这要么会提前停止测试(如果 assert 调用了 t.Fatalf
或 panic
),要么会遗漏有关测试正确之处的有趣信息。它还迫使 assert 包创建一整套新的子语言,而不是重用现有的编程语言(Go 本身)。Go 对打印结构体提供了良好的支持,所以一个更好的方法是这样写代码:
if obj == nil || obj.Type != "blogPost" || obj.Comments != 2 || obj.Body == "" {
t.Errorf("AddPost() = %+v", obj)
}
断言库使得编写不精确的测试过于容易,并不可避免地会重复实现语言中已有的功能,如表达式求值、比较,有时甚至更多。努力编写精确的测试,既能说明哪里出错了,也能说明哪里是对的,并利用 Go 本身,而不是在 Go 中创建一种迷你语言。
选择易于人类阅读的子测试名称
当你使用 t.Run
创建子测试时,第一个参数用于为测试提供描述性名称。为了确保测试结果对阅读日志的人来说是易读的,请选择在转义后仍然有用且可读的子测试名称。(测试运行器会将空格替换为下划线,并转义非打印字符)。
要标识输入,请在子测试的函数体内使用 t.Log
,或将其包含在测试的失败消息中,这样它们就不会被测试运行器转义。
比较完整结构
如果你的函数返回一个结构体,不要编写对结构体中每个字段进行单独比较的测试代码。相反,构造一个你期望函数返回的结构体,然后一次性进行比较,使用差异或深度比较。相同的规则也适用于数组和 map。
如果你的结构体需要进行近似相等性或其他类型的语义相等性比较,或者它包含无法进行相等性比较的字段(例如,如果其中一个字段是 io.Reader
),那么调整 cmp.Diff
或 cmp.Equal
比较,并使用 cmpopts
选项,例如 cmpopts.IgnoreInterfaces
,可能就能满足你的需求(示例);否则,这种技术就无法实现,请使用任何有效的方法。
如果你的函数返回多个返回值,你不需要在比较之前将它们包装成一个结构体。只需单独比较返回值并打印它们。
比较稳定结果
避免比较可能固有地依赖于你无法控制的外部包的输出稳定性。相反,测试应该比较稳定的、对依赖项更改具有抵抗力的语义相关信息。对于返回格式化字符串或序列化字节的功能,通常不能假定输出是稳定的。
例如, json.Marshal
不保证它可能发出的确切字节。它有自由(并且过去也曾)更改输出。进行字符串相等性比较以匹配确切 JSON 字符串的测试,在 json
包更改其序列化字节的方式时可能会失败。相反,一个更健壮的测试将解析 JSON 字符串的内容,并确保它在语义上等同于某个预期的数据结构。
相等比较和差异
==
运算符使用语言定义的比较来评估相等性。它可以比较的值包括数字、字符串和指针值,以及包含这些值字段的结构体。特别是,它仅当两个指针指向同一个变量时才认为它们相等。
使用 cmp
包。使用 cmp.Equal
进行相等比较,使用 cmp.Diff
获取对象之间易于人类阅读的差异。
虽然 cmp
包不是 Go 标准库的一部分,但它由 Go 团队维护,并且应该在 Go 版本更新之间产生稳定的结果。它是用户可配置的,应该能满足大多数比较需求。
你会发现旧代码使用标准的 reflect.DeepEqual
函数来比较复杂结构。对于新代码,优先使用 cmp
,并在实际可行的情况下考虑更新旧代码以使用 cmp
。reflect.DeepEqual
对未导出字段和其他实现细节的变化很敏感。
注意:cmp
包也可以与协议缓冲区消息一起使用,方法是在比较协议缓冲区消息时包含 cmp.Comparer(proto.Equal)
选项。
先 Got 后 Want
测试输出应先输出函数返回的实际值,然后再打印期望值。打印测试输出的常用格式是“YourFunc(%v) = %v, want %v
”。
对于差异,方向性不太明显,因此包含一个键来帮助解释失败非常重要。参见打印差异。
无论你在失败消息中使用哪种顺序,都应该在失败消息中明确指明顺序,因为现有代码在这个顺序上并不一致。
标识函数
在大多数测试中,失败消息应包含失败函数的名称,即使从测试函数的名称中看起来很明显。
优先
t.Errorf("YourFunc(%v) = %v, want %v", in, got, want)
而不是
t.Errorf("got %v, want %v", got, want)
标识输入
在大多数测试中,你的测试失败消息应该包含函数输入(如果它们很简短)。如果输入的关键属性不明显(例如,因为输入很大或不透明),你应该用描述所测试内容的名称来命名你的测试用例,并在错误消息中打印该描述。
不要使用测试表中测试的索引来代替命名你的测试或打印输入。没有人愿意查看你的测试表并数数才能弄清楚哪个测试用例失败了。
继续进行
即使在测试用例遇到失败后,它们也应该尽可能地继续进行,以便在一次运行中打印出所有失败的检查。这样,修复失败测试的人就不必玩“打地鼠”游戏,修复一个错误,然后重新运行测试来查找下一个错误。
从实际角度来看,优先调用 t.Error
而不是 t.Fatal
。在比较函数输出的几个不同属性时,对每个比较都使用 t.Error
。
t.Fatal
通常只适用于一些测试设置失败,没有这些设置就无法运行测试。在表驱动测试中,t.Fatal
适用于在测试循环之前设置整个测试的功能的失败。影响测试表中单个条目的失败,使得无法继续处理该条目,应按如下方式报告:
- 如果你不使用
t.Run
子测试,你应该调用t.Error
,然后跟一个continue
语句来继续处理下一个表条目。 - 如果你正在使用子测试(并且你是在调用
t.Run
的内部),那么t.Fatal
会结束当前子测试并允许你的测试用例继续到下一个子测试,所以请使用t.Fatal
。
标记测试助手
测试助手是一个执行设置或拆卸任务的函数,例如构造输入消息,而该任务不依赖于被测试的代码。
如果你传递一个 *testing.T
,请调用 t.Helper
来将测试助手中的失败归因于调用助手的行。
func TestSomeFunction(t *testing.T) {
golden := readFile(t, "testdata/golden.txt")
// ...
}
func readFile(t *testing.T, filename string) string {
t.Helper()
contents, err := ioutil.ReadFile(filename)
if err != nil {
t.Fatal(err)
}
return string(contents)
}
当它会模糊测试失败与导致失败的条件之间的联系时,不要使用此模式。特别是,t.Helper
不应用于实现断言库。
打印差异
如果你的函数返回大型输出,那么阅读失败消息的人可能很难在测试失败时找到差异。不要同时打印返回值和期望值,而是制作一个差异。
在失败消息中添加一些文本来解释差异的方向。
当你使用 cmp
包时(如果你将 (want, got)
传递给函数),类似“diff -want +got
”的内容是很好的,因为你在格式字符串中添加的 -
和 +
将与差异行开头实际出现的 +
和 -
匹配。
差异会跨越多行,所以你应该在打印差异之前打印一个换行符。
表驱动测试与多个测试函数
表驱动测试应该在许多不同的测试用例可以使用类似的测试逻辑来测试时使用,例如在测试函数的实际输出是否等于预期输出时示例,或者在测试函数的输出总是符合同一组不变量时。
当某些测试用例需要使用与其他测试用例不同的逻辑进行检查时,编写多个测试函数更为合适。当表中的每个条目都需要经过多种条件逻辑来为正确的输入执行正确的输出检查时,你的测试代码的逻辑会变得难以理解。如果它们具有不同的逻辑但设置相同,那么在单个测试函数中使用一系列子测试也可能是合理的。
你可以将表驱动测试与多个测试函数结合起来。例如,如果你正在测试一个函数的非错误输出是否与预期输出完全匹配,并且你还在测试该函数在输入无效时返回某个非 nil 错误,那么通过编写两个单独的表驱动测试函数——一个用于正常非错误输出,一个用于错误输出——可以实现最清晰的单元测试。
测试错误语义
当单元测试执行字符串比较或使用 reflect.DeepEqual
来检查是否为特定输入返回了特定类型的错误时,如果你将来必须重写任何错误消息,你可能会发现你的测试很脆弱。由于这有可能将你的单元测试变成一个变更检测器,所以不要使用字符串比较来检查你的函数返回的错误类型。
使用字符串比较来检查被测包中的错误消息是否满足某些属性是可以的,例如,它是否包含参数名称。
如果你关心测试函数返回的确切错误类型,你应该将用于人类阅读的错误字符串与为程序化使用而公开的结构体分开。在这种情况下,你应该避免使用 fmt.Errorf
,它倾向于破坏语义错误信息。
许多编写 API 的人并不确切地关心他们的 API 对不同输入返回哪种类型的错误。如果你的 API 是这样的,那么使用 fmt.Errorf
创建错误消息就足够了,然后在单元测试中,只测试在你期望错误时错误是否为非 nil。
此内容是 Go Wiki 的一部分。