Go单元测试

编写测试源码

使用go的测试框架需要遵循一定的规则,运行 go test 命令将执行当前目录下的包的测试代码,它会寻找 *_test.go 文件,并在这些文件中,寻找符合 TestXxx(*testing.T){} 命名的函数和参数(即,接收 *testing.T 参数的函数,命名为 TestXxxXxx 可以是任何不以小写字符开头的名字)。这个测试代码不会影响正常的编译过程,只在执行 go test 时被使用。

源文件

1
2
3
4
5
6
7

// gomod.go
package gomod
import "fmt"
func Say() {
fmt.Println("hello everybody!")
}

测试文件

1
2
3
4
5
6
7

// gomod_test.go
package gomod
import "testing"
func TestGomod(t *testing.T) {
Test()
}

运行测试代码

go test

1
2
this is a test!PASS
ok gomod 0.705s

go test -v

1
2
3
4
=== RUN   TestGomod
this is a test!--- PASS: TestGomod (0.00s)
PASS
ok gomod 0.845s

跳过测试

一些测试可能要求要有特定的上下文环境。例如,一些测试可能需要调用一个外部的命令,使用一个特殊的文件,或者需要一个可以被设置的环境变量。当条件无法满足时,(如果)不想让那些测试失败,可以简单地跳过那些测试:

1
2
3
4
5
6
func TestSkip(t *testing.T) {
if os.Getenv("SKIP") == "" {
t.Skip("skipping test; $SKIP not set")
}
// ... the actual test
}

运行效果

1
2
3
4
5
6
=== RUN   TestSkip
--- SKIP: TestSkip (0.00s)
gomod_test.go:12: skipping test; $SKIP not set
PASS
ok gomod 0.777s

通常是用 -short 命令行标志来实现这个跳过的特性,如果标志被设置的话,反映到代码中,testing.Short() 将简单地返回 true(就像是 -v 标志一样,如果它被设置,通过判断 testing.Verbose() ,你可以打印出额外的调试日志)。

1
2
3
4
5
6
func TestShort(t *testing.T) {
if testing.Short() {
t.Skip("skipping malloc count in short mode")
}
// rest of test...
}

运行go test -v -short效果如下

1
2
3
4
5
=== RUN   TestShort
--- SKIP: TestShort (0.00s)
gomod_test.go:19: skipping malloc count in short mode
PASS
ok gomod 0.811s

-timeout 标志,它能够被用来强制退出限定时间内没有运行完的测试。例如,运行这个命令 go test -timeout 1s 以执行下面的测试:

1
2
3
4
5
func TestTimeout(t *testing.T) {
time.Sleep(2 * time.Second)
// pass if timeout > 2s
}

运行go test -timeout 1s 效果如下

1
2
3
4
5
6
7
=== RUN   TestTimeout
panic: test timed out after 1s

goroutine 18 [running]:
testing.(*M).startAlarm.func1()
F:/Go/src/testing/testing.go:1334 +0xe6
...

执行特定测试函数

go test -v -run TestGomod

1
2
3
4
5
=== RUN   TestGomod
this is a test!
--- PASS: TestGomod (0.00s)
PASS
ok gomod 0.820s

并行执行测试

默认情况下,指定包的测试是按照顺序执行的,但也可以通过在测试的函数内部使用t.Parallel() 来标志某些测试也可以被安全的并发执行(和默认的一样,假设参数名为 t)。在并行执行的情况下,只有当那些被标记为并行的测试才会被并行执行,所以只有一个测试函数时是没意义的。它应该在测试函数体中第一个被调用(在任何需要跳过的条件之后),因为它会重置测试时间:

1
2
3
4
func TestParallel(t *testing.T) {
t.Parallel()
// actual test...
}

在并发情况下,同时运行的测试的数量默认取决于 GOMAXPROCS。它可以通过 -parallel n 被指定(go test -parallel 4

另外一个可以实现并行的方法,尽管不是函数级粒度,但却是包级粒度,就是类似这样执行 go test p1 p2 p3(也就是说,同时调用多个测试包)。在这种情况下,包会被先编译,并同时被执行。当然,这对于总的时间来说是有好处的,但它也可能会导致错误变得具有不可预测性,比如一些资源被多个包同时使用时(例如,一些测试需要访问数据库,并删除一些行,而这些行又刚好被其他的测试包使用的话)。

为了保持可控性,-p 标志可以用来指定编译和测试的并发数。当仓库中有多个测试包,并且每个包在不同的子目录中,一个可以执行所有包的命令是 go test ./...,这包含当前目录和所有子目录。没有带 -p 标志执行时,总的运行时间应该接近于运行时间最长的包的时间(加上编译时间)。运行 go test -p 1 ./...,使编译和测试工具只能在一个包中执行时,总的时间应该接近于所有独立的包测试的时间加上编译的时间的总和。你可以自己试试,执行 go test -p 3 ./...,看一下对运行时间的影响。

还有,另外一个可以并行化的地方(你应该测试一下)是在包的代码里面。多亏了 Go 非常棒的并行原语,实际上,除非 GOMAXPROCS 通过环境变量或者在代码中显式设置为 GOMAXPROCS=1,否则,包中一个goroutines 都没有用是不太常见的。想要使用 2 个 CPU,可以执行 GOMAXPROCS=2 go test,想要使用 4 个 CPU,可以执行 GOMAXPROCS=4 go test,但还有更好的方法:go test -cpu=1,2,4 将会执行 3 次,其中 GOMAXPROCS 值分别为 1,2,和 4。

-cpu 标志,搭配数据竞争的探测标志 -race,简直进入天堂(或者下地狱,取决于它具体怎么运行)。竞争探测是一个很神奇的工具,在以高并发为主的开发中不得不使用这个工具(来防止死锁问题),但对它的讨论已经超过了本文的范围。如果你对此感兴趣,可以阅读 Go 官方博客的 这篇文章

在你写自己的测试代码前,建议看一下标准库中的 testing/iotesttesting/quicknet/http/httptest 软件包。

总结

go test 默认执行当前目录下以xxx_test.go的测试文件。
go test -v 可以看到详细的输出信息。
go test -v xxx_test.go 指定测试单个文件,但是该文件中如果调用了其它文件中的模块会报错。

指定某个测试函数运行:
go test -v -test.run Testxxx
注意: 该测试会测试包含该函数名的所有函数,即如果待测试的函数名是TestSyncResourceQuota,那么指令go test -v -test.run TestSyncResourceQuota会测试包含该函数名的所有函数(比如下面的TestSyncResourceQuotaSpecChange、TestSyncResourceQuotaSpecHardChange等函数)

没有指定测试文件时会执行当前目录下的init()函数