Go Wiki:Go 测试注释

本页面是对 Go 代码审查注释 的补充,但专门针对测试代码。

请在编辑此页面之前 讨论更改,即使是次要更改。许多人有自己的意见,这里不适合进行编辑争论。

断言库

避免使用“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.Fatalfpanic),或者省略测试正确内容的有趣信息。它还迫使 assert 包创建一种全新的子语言,而不是重新使用现有的编程语言(Go 本身)。Go 对打印结构有很好的支持,因此编写此代码的更好方法是

if obj == nil || obj.Type != "blogPost" || obj.Comments != 2 || obj.Body == "" {
    t.Errorf("AddPost() = %+v", obj)
}

Assert 库使得编写不精确的测试变得过于容易,并且最终会重复语言中已有的功能,如表达式求值、比较,有时甚至更多。努力编写既准确说明出错内容又准确说明正确内容的测试,并使用 Go 本身,而不是在 Go 中创建一种迷你语言。

选择可读的子测试名称

当你使用 t.Run 创建子测试时,第一个参数用作测试的描述性名称。为了确保测试结果对阅读日志的人类来说是可读的,请选择在转义后仍然有用且可读的子测试名称。(测试运行器用下划线替换空格,并转义不可打印的字符)。

识别输入,请在子测试的主体中使用 t.Log,或将其包含在测试的失败消息中,在这些消息中,它们不会被测试运行器转义。

比较完整结构

如果你的函数返回一个结构,请不要编写对结构的每个字段执行单独比较的测试代码。相反,构造你希望你的函数返回的结构,并使用 差异深度比较 一次性进行比较。相同的规则适用于数组和映射。

如果你的结构需要比较近似相等或某种其他语义相等,或者它包含无法比较相等的字段(例如,如果其中一个字段是 io.Reader),则调整 cmp.Diffcmp.Equal 比较与 cmpopts 选项(如 cmpopts.IgnoreInterfaces)可能满足你的需求(示例);否则,此技术将不起作用,因此请执行任何有效的方法。

如果你的函数返回多个返回值,则无需在比较它们之前将它们包装在结构中。只需单独比较返回值并打印它们即可。

比较稳定结果

避免比较可能本质上依赖于您无法控制的某些外部包的输出稳定性的结果。相反,测试应比较语义相关的信息,这些信息稳定且能够抵抗依赖关系的变化。对于返回格式化字符串或序列化字节的功能,通常不安全地假设输出是稳定的。

例如,json.Marshal 不会对它可能发出的确切字节做出任何保证。它有权更改(并且过去已经更改)输出。如果 json 包更改了它序列化字节的方式,对确切 JSON 字符串执行字符串相等性的测试可能会中断。相反,更健壮的测试将解析 JSON 字符串的内容并确保它在语义上等效于某些预期的数据结构。

相等比较和差异

== 运算符使用语言定义的比较来评估相等性。它可以比较的值包括数字、字符串和指针值以及具有这些值字段的结构。特别是,它仅当两个指针指向同一变量时才确定它们相等。

使用cmp包。对于相等性比较,使用cmp.Equal;对于对象之间的可读差异,使用cmp.Diff

尽管 cmp 包不是 Go 标准库的一部分,但它由 Go 团队维护,并且应该在 Go 版本更新中产生稳定的结果。它是用户可配置的,应该满足大多数比较需求。

您会发现较旧的代码使用标准 reflect.DeepEqual 函数来比较复杂结构。对于新代码,优先使用 cmp,并在实际情况下考虑将较旧的代码更新为使用 cmpreflect.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 适用于在测试循环之前设置整个测试函数的失败。影响测试表中单个条目的失败(导致无法继续使用该条目)应按如下方式报告

标记测试助手

测试帮助器是一个执行设置或拆除任务的函数,例如构建输入消息,该消息不依赖于被测代码。

如果传递 *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 不应用于实现断言库。

如果函数返回大量输出,那么在测试失败时,阅读失败消息的人可能很难找到差异。不要同时打印返回值和所需值,而是进行比较。

在错误消息中添加一些文本,说明 diff 的方向。

如果你使用的是 cmp 包(如果你将 (want, got) 传递给函数),则类似于“diff -want +got”的内容很好,因为你添加到格式字符串中的 -+ 将与实际出现在 diff 行开头的 +- 匹配。

diff 将跨越多行,因此你应在打印 diff 之前打印一个换行符。

表驱动测试与多测试函数

表驱动测试应在可以使用类似测试逻辑测试许多不同测试用例时使用,例如在测试函数的实际输出是否等于预期输出示例时,或在测试函数的输出是否始终符合同一组不变量时使用。

当某些测试用例需要使用不同于其他测试用例的逻辑进行检查时,编写多个测试函数更为合适。当表中的每个条目都必须经过多种条件逻辑以对正确类型的输入进行正确类型的输出检查时,测试代码的逻辑可能会难以理解。如果它们具有不同的逻辑但设置相同,则单个测试函数中的子测试序列也可能是有意义的。

你可以将表驱动测试与多个测试函数结合使用。例如,如果你正在测试函数的非错误输出是否与预期输出完全匹配,并且你还正在测试函数在获取无效输入时是否返回一些非 nil 错误,则可以通过编写两个单独的表驱动测试函数来实现最清晰的单元测试——一个用于常规非错误输出,另一个用于错误输出。

测试错误语义

当单元测试执行字符串比较或使用 reflect.DeepEqual 检查特定类型的错误是否针对特定输入返回时,你可能会发现,如果你将来必须重新表述任何这些错误消息,你的测试将很脆弱。由于这有可能将你的单元测试变成更改检测器,因此请不要使用字符串比较来检查函数返回的错误类型。

使用字符串比较来检查来自被测包的错误消息是否满足某些属性是可以的,例如,它包括参数名称。

如果你关心测试函数返回的错误的准确类型,则应将面向人眼的错误字符串与面向编程使用的结构分开。在这种情况下,你应避免使用 fmt.Errorf,它往往会破坏语义错误信息。

许多编写 API 的人并不关心他们的 API 为不同的输入返回哪种类型的错误。如果你的 API 是这样的,那么使用 fmt.Errorf 创建错误消息就足够了,然后在单元测试中,只测试在你期望出现错误时错误是否为非空。


此内容是 Go Wiki 的一部分。