官方文档
https://github.com/golang/go/wiki/TestComments
断言
测试的时候尽量避免使用断言,下面是常用的断言例子
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, "")
这种断言会导致测试用例过早的被终止,有时会漏掉很多关键的信息。测试应该是精准的,能一眼看过出哪部分导致的用例失败,哪部分是成功的。并且第三方的封装,并不一定是以你的真实期望断言(如果你不看一下源码)。
所以上面的例子可以改为:
if obj == nil || obj.Type != "blogPost" || obj.Comments != 2 || obj.Body == "" {
t.Errorf("AddPost() = %+v", obj)
}
使用可读性高的子测试用例名称
当使用t.Run来创建子测试时候,第一个参数是用例名称。为了确保测试结果在日志上具备高可读性,用例名称应该描述测试的场景,并保证转义可读(测试用例执行时,会将空格转换为下划线,并转义不可打印的字符)。
可以在子测试函数体使用t.Log打印子测试用例名称。
直接比较结构体
使用cmp包
package main
import (
"fmt"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
type aType struct {
FieldA string
UncomparableFieldB Uncomparable
}
type UncomparableType struct{
greeting string
}
type Uncomparable interface {
Greeting() string
}
func (u UncomparableType) Greeting() string {
return u.greeting
}
func main() {
a := aType{FieldA: "hello"}
b := aType{
FieldA: "hello",
UncomparableFieldB: UncomparableType{
greeting: "bonjour",
},
}
if diff := cmp.Diff(a, b, cmpopts.IgnoreInterfaces(struct{Uncomparable}{})); diff != "" {
fmt.Printf("differences, %s", diff)
}
if eq := cmp.Equal(a, b, cmpopts.IgnoreInterfaces(struct{Uncomparable}{})); eq != true {
fmt.Printf("not equal!")
}
}
只比较稳定的结果
如果被测函数依赖的外部包不受控制,导致输出结果不稳定,就该避免在测试中使用该结果。相反,应该去比较那些在语义上稳定的信息。
相等性比较
使用cmp包的cmp.Equal可直接比较两个任意的对象,使用cmp.Diff则会输出这两个对象间的差异,而且可读性非常高。
老旧的代码中会使用reflect.DeepEqual函数来比较复杂的结构体,现在建议用cmp包来代替,因为reflect.DeepEqual对一些未导出的字段和实现细节的变动非常敏感。
(cmp包使用时添加cmp.Compare(proto.Equal)选项即可直接用于 protocol buffer 消息的比较。)
不仅打印期望值,也要打印实际值
典型例子
YourFunc(%v) = %v, want %v
标识函数
prefer:
t.Errorf("YourFunc(%v) = %v, want %v", in, got, want)
and ont:
t.Errorf("got %v, want %v", got, want)
标识输入
在大部分测试中,函数输入参数也应该包含在失败消息中。如果输入参数的相关属性不明显(比如,参数较大或晦涩难懂),你应该在测试名中描述本测试的内容,并且将描述信息放入错误消息中。
失败继续执行
即使测试用例遇到了失败,也该继续执行,这样就可以一次性测出多个可能存在的问题,避免了每改一次执行一次。
使用t.Error代替t.Fatal。当比较函数的多个输出时,对每一个分别使用t.Error。
t.Fatal比较适合在setup中使用,因为setup一旦失败,后续步骤也就没有意义。表格驱动的测试中,t.Fatal适合在所有子测试开始前使用。表格中的每一测试用例若遇到不可恢复的错误,如何处理要分具体情况:
- 如果你没有使用t.Run运行子测试,那应该使用t.Error并使用conitnue语句直接跳转到下一项用例。
- 如果你使用t.Run运行子测试,那t.Fatal只会中断当前用例,其余子测试会继续执行。
标记测试辅助函数
辅助函数通常在setup,teardown中,比如构造一个测试数据。
示例
标记helper之前
package main
import "testing"
func testHelper(t *testing.T) {
t.Fatal()
}
func TestHelloWorld(t *testing.T) {
testHelper(t)
}
输出
--- FAIL: TestHelloWorld (0.00s)
main_test.go:6:
FAIL
FAIL test.test 0.001s
FAIL
标记helper
package main
import "testing"
func testHelper(t *testing.T) {
t.Helper()
t.Fatal()
}
func TestHelloWorld(t *testing.T) {
testHelper(t)
}
输出
--- FAIL: TestHelloWorld (0.00s)
main_test.go:11:
FAIL
FAIL test.test 0.002s
FAIL
显示错误的位置不同了,后者直接定位在了调用testHelper的测试用例处,这样在多个调用中也很清晰的知道谁调用出错了。
打印不同Diff
前面cmp包可用
如果函数返回的输出比较长,而出错的地方只是其中一小段,那很难一眼看出区别。这对调试不友好,建议直接输出期望和实际结果的 diff 值。
表格驱动测试vs多个测试函数
当多个测试用例只是数据不同,测试逻辑没问题,那么就应该使用表格驱动测试。
表格测试示例
而当每个测试用例需用不同的方法验证时,表格驱动就显得不合适,因为那样就不得不写一堆控制变量放入表格中,将原本的测试逻辑淹没其中,降低了用例的可读性和表格的可维护性。
实际测试两种方法需结合使用。比如可以写两个表格驱动测试方法,一个测试函数的正常返回结果,另一个测试不同错误消息。
测试错误语义
当一个单元测试执行字符串比较或使用reflect.DeepEqual来检查对特定的输入返回特定类型的错误时,如果你不得不在将来重新编写任何这些错误信息,你可能会发现你的测试是脆弱的。由于这有可能将你的单元测试变成一个变化检测器,所以不要使用字符串比较来检查你的函数返回的错误类型。
使用字符串比较来检查来自被测包的错误信息是否满足某些属性是可以的,例如,它包括参数名。
如果你关心的是测试你的函数返回的错误类型,你应该把打算给人看的错误字符串和暴露给程序使用的结构分开。在这种情况下,你应该避免使用fmt.Errorf,它往往会破坏语义错误信息。
很多写API的人并不关心他们的API对不同的输入到底会返回什么样的错误。如果你的API是这样的,那么使用fmt.Errorf创建错误信息就可以了,然后在单元测试中,只测试你预期的错误是否为非nil。
总结
其实日常基本都是第三方库一把梭了,看完这个文档对测试还是有了点新的理解,当然还是没有银弹。