golang test 要求我们以 *_test.go
来创建测试文件,注意 以 _
包括(_test.go
) 或者 .
开头的文件都会被忽略
测试文件中可以包含 测试函数,基准测试函数, 示例函数
- 测试函数必须以
TestXxx
命名,其中Xxx不能小写字母开头 - 基准测试函数必须以
BenchmarkXxx
命名 - 示例函数必须以
ExampleXxx
命名
go build 时,并不会将 *_test.go 文件一起打包编译,所以 *_test.go 中的一些 init 或者全局变量,并不能在执行中访问
go test
会编译当前目录中的包和测试文件,然后将生成的二进制文件存放到临时目录,然后运行测试二进制文件
,执行完成后便删除文件
生成测试二进制可执行文件
如果只想编译测试二进制
执行go test -c
将测试二进制编译为 .test 的二进制文件,并且不会运行测试
可以直接执行测试二进制文件来测试 ./<pkg-name>.test
如果在运行完测试后,保留测试的二进制文件
执行 go test -o <filename>
-o 指定二进制文件的名字
测试二进制文件可以通过 -test.<flag>
来使用一些 go test 的 flag
比如 打印测试日志 ./<pkg-name>.test -test.v
测试其他包或者目录
go test
在不指定想要的测试的包的时候会查找当前目录的包和测试文件,并且不会使用缓存
go test <pkg>
可以指定包名称,他会在 GOROOT 中查找包
go test .
会在当前目录执行测试
go test ./...
可以遍历子目录来执行测试
测试缓存
在指定了包或者目录的情况下,Go 会测试成功的结果,以避免重复运行相同的测试
- 显式禁用测试缓存,使用
-count=1
flag - 禁用全局缓存,可以设置 GOCACHE 环境变量
go env -w GOCACHE=off
也可以手动清理缓存 go clean -testcache
测试覆盖率
-cover
开启覆盖分析
-coverprofile cover.out
:在所有测试通过后,将测试覆盖报告写到文件中。
配合 -covermode
可以显示代码被覆盖的次数,也就是代码的测试热度
-covermode set
为默认 flag,只显示代码是否被覆盖到
-covermode count
显示代码被测试覆盖的次数
go tool cover -html=c.out
直接通过浏览器查看报告
go tool cover -html=cover.out -o cover.html
子测试
t.Run(name, t func(t *testing.T))
子测试中可以使用 t.Parallel()
来并行执行测试,不过父级测试会等到所有子测试完成,t.Run() 才会返回
func TestTeardownParallel(t *testing.T) {
// This Run will not return until the parallel tests finish.
t.Run("group", func(t *testing.T) {
t.Run("Test1", parallelTest1)
t.Run("Test2", parallelTest2)
t.Run("Test3", parallelTest3)
})
// tear-down code
}
-run
执行指定的测试
默认,go test 会执行包中所有的测试函数,并打印测试结果,无论每个测试是否成功或者失败
go test <filename>...
可以测试指定的测试文件
可以通过 -run regexp
来执行指定的测试函数或者子测试函数
执行所有测试 go test -run ''
,相当于 go test
/ go test .
执行包含 Foo 的测试 go test -run Foo
执行包含 Foo 测试的 Sub2 子测试 go test -run Foo/Sub2
只执行子测试名字为 Sub1 的子测试 go test -run /Sub1
提供对 -short
的支持
go test 如果设置了 -short
标志,那么测试中 testing.Short() 会返回 true
如果在测试中增加了 short 判断,那么就可以跳过一些运行时间很长的测试函数
func TestPICalculation(t *testing.T){
if testing.Short() {
t.Skip("skipping PI Calculation in short model")
}
}
-short 标志只是一个标示,具体还依赖于开发者,是否需要避免一些运行比较慢的函数被执行。
测试中常用的函数
func TestA (t *testing.T){...}
打印日志
t.Log
类似 fmt.Println()
t.Logf
类似 fmt.Prinltn(fmt.Sprintf())
标记测试失败
t.Fail
标记函数测试失败,但是会继续执行,不会中断测试函数
t.FailNow
标记失败,并调用 runtime.Goexit
终止当前测试
如果在测试函数中创建的 goroutine 中执行
t.FailNow
的话,会退出 goroutine,并不会中断测试函数
跳过测试
t.SkipNow
将测试标记为跳过,并调用 runtime.Goexit 中断测试函数
衍生函数
t.Fatalf
= t.Logf
+ t.FailNow
t.Fatal
= t.Log
+ t.FailNow
t.Error
= t.Log
+ t.Fail
t.Errorf
= t.Logf
+ t.Fail
t.Skip
= t.Log
+ t.SkipNow
t.Skipf
= t.Logf
+ t.SkipNow
如果测试函数先被标记为失败,再被标记为跳过,那么依然为失败
t.CleanUp
注册一个在测试及其所有子测试完成时调用的函数。清理函数将按“最后添加,先调用”的顺序调用。
t.TempDir
返回测试要使用的临时目录,当测试完成后,会调用 t.CleanUp
自动删除该目录
黑盒测试与循环引用问题
通常情况下,测试和要被测试的代码在同一个包中,这样才能访问内部实现细节的代码。
但是当包出现循环引用时,测试文件就无法被正确编译了
为了解决这个问题,go test 支持使用测试文件的包名以 <pkg>_test
后缀命名,并被编译成独立的包
这种机制也被称为提供了黑盒测试
package foo_test
import (
"foo/mock" // also imports foo
"foo"
)
这时只能调用 foo 包中的公开变量和函数
黑盒测试文件虽然和被测试的包处于同一个目录中,但是依然需要通过 foo 包名来调用变量或函数
可以使用 import .
来假装测试文件是 foo 包的一部分,注意 foo 包中的私有变量 和 函数,依然不会导入进来
package foo_test
import (
"foo/mock" // also imports foo
. "foo"
)
在其他情况下,不要使用 import . ,因为可能会引起变量混乱的问题
测试前的初始化准备工作
为了设置环境或者为了避免测试数据污染,有时候需要一些初始化和测试结束的清理操作,比如在所有的测试开始的前后清空某个测试数据库中的内容等。
这样的任务如果在每个测试用例中都重复执行,那不仅是的代码冗余,也是资源的浪费。
可以通过 TestMain
来帮我们执行初始化和清理操作
func TestMain(m *testing.M){
doSomeSetup()
r := m.Run()
doSomeClear()
os.Exit(r)
}
TestMain 函数是 GO 测试框架的入口点,一个包下只能有一个 TestMain
函数,通过调用 m.Run
来执行其他测试。
如果没有在所有测试的前后执行一些任务,那么就不需要实现 TestMain
。
对于单个测试,或者多个子测试进行清理操作可以使用 t.CleanUp
函数,会在测试及其所有子测试完成时调用的函数
并发测试
默认情况下,指定包的测试是按照顺序执行的,但也可以通过在测试的函数内部使用 t.Parallel()
来标志某些测试也可以被安全的并发执行。
func TestParallel(t *testing.T) {
t.Parallel()
// ...
}
在并行执行的情况下,只有那些被标记为并行的测试才会被并行执行,所以只有一个测试函数时是没意义的。
它应该在测试函数体中第一个被调用(在任何需要跳过的条件之后),因为它会重置测试时间
并行测试的数量默认取决于 GOMAXPROCS,可以通过 -parallel n
来指定测试的最大并发数
包级并发测试
调用 go test ./...
测试所有子目录中的包,或者 go test <pkg1> <pkg2>
指定测试多个包时,包会被先编译,并同时被执行(并发执行)。
这对于总的时间来说是有好处的,但它也可能会导致错误变得具有不可预测性,比如一些资源被多个包同时使用时(例如,一些测试需要访问数据库,并删除一些行,而这些行又刚好被其他的测试包使用的话)。
为了保持多包测试的可控性,-p
标志可以用来指定编译和测试的并发数。
- 没有带 -p 标志执行时,总的测试时间应该接近于运行时间最长的包的时间。
-p 1
使编译和测试工具只能在一个包中执行时,总的测试时间应该接近于所有独立的包测试的时间之和
helper 函数
对于一些重复的逻辑,可以抽取出来作为公共的帮助函数(helpers),增加测试代码的可读性和可维护性
借助帮助函数,可以让测试用例的主逻辑看起来更清晰
正常的函数会报告错误发生的文件和行号信息
如果在函数中调用了 t.Helper()
, 那么就会将函数标注为帮助函数,报错是将输出帮助函数调用者的信息,而不是帮助函数中的内部信息
其他 test flag
-args
可以传递命令行参数给测试文件-i
安装与测试相关的包,不运行测试-v
是用于输出所有Log的信息, 可以打印一些额外的调试日志以及每个测试函数的执行情况-json
将输出转换成 json 格式-failfast
指定如果测试失败,会立即停止其他测试-list regexp
只列出匹配成功的测试函数,不会真正执行,并且不会列出子函数-timeout d[s|m|h]
默认测试执行超过10分钟会超时退出
性能测试
go test 默认不执行性能测试,只有使用 -bench
才运行性能测试,并且只运行性能测试
-bench regexp
可以接正则,来匹配需要执行的性能测试
-bench .
-bench=.
可以执行所有基准测试
性能测试同样可以包含子测试
func BenchmarkSub(b *testing.B){
b.Run("A", benchSub1)
b.Run("b", benchSub2)
}
-bench Sub/A
指定子测试
-benchtime <t>s
指定每个性能测试的执行时间,默认为 1s
-cpu n1, n2
提供 CPU 个数的列表,测试会按照列表指定的 CPU 数设置 GOMAXPROCS 并测试
-cpu 1,2,3
每个测试会执行3次,分别使用 1,2,3 个cpu
-count n
每个测试执行的次数,默认执行一次
如果同时指定了 -count
,-cpu
那么测试会在每种 cpu 数量下执行 count 指定的次数
-benchmem
可以打印每个操作分配的字节数,每个操作分配的对象数-cpuprofile cpu.out
:在退出之前,将一个 CPU 概要文件写入指定的文件。-memprofile mem.out
:在所有测试通过后,将内存概要文件写到文件中。-memprofilerate n
:开启更精确的内存配置。如果为 1,将会记录所有内存分配到 profile。