本节项目地址:07-UnitTestBenchmarkTest
Go语言中的测试依赖go test
,该命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go
为后缀名的源代码文件都是go test
测试的一部分,不会被go build
编译到最终的可执行文件中。
在*_test.go
文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。
类型 | 格式 | 作用 |
---|---|---|
测试函数 | 函数名前缀为Test |
测试程序的一些逻辑行为是否正确 |
基准函数 | 函数名前缀为Benchmark |
测试函数的性能 |
示例函数 | 函数名前缀为Example |
为文档提供示例文档 |
go test
命令会遍历所有的*_test.go
文件中符合上述命名规则的函数,然后生成一个临时的main
包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。
func TestFuncName(t *testing.T) {}
Test
开头,可选的后缀名以大写字母开头*testing.T
类型,用于报告测试失败和附加的日志信息下述创建了一个测试函数:
// TestStringSplit 测试 string 的 split 函数
func TestStringSplit(t *testing.T) {
// 定义一个测试用例类型
type test struct {
input string
sep string
want []string
}
// 定义一个存储测试用例的切片
tests := []test{
{input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
{input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
{input: "abcd", sep: "bc", want: []string{"a", "d"}},
{input: "沙河有沙又有河", sep: "沙", want: []string{"", "河有", "又有河"}},
}
// 遍历切片,逐一执行测试用例
for _, tc := range tests {
got := strings.Split(tc.input, tc.sep)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("expected:%#v, got:%#v", tc.want, got)
}
}
}
go test -v
:执行测试,并打印出测试函数名和运行时间go test -v
=== RUN TestStringSplit
--- PASS: TestStringSplit (0.00s)
PASS
ok 07-UnitTestBenchmarkTest/UnitTest 0.662s
如果测试用例比较多的时候,我们是没办法一眼看出来具体是哪个测试用例失败了。可以按照如下方式使用t.Run
执行子测试就能够看到更清晰的输出内容了。
// TestStringSplit1 测试 string 的 split 函数,子测试
func TestStringSplit1(t *testing.T) {
// 定义一个测试用例类型
type test struct {
input string
sep string
want []string
}
// 测试用例使用map存储
tests := map[string]test{
"simple": {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
"wrong sep": {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
"more sep": {input: "abcd", sep: "bc", want: []string{"a", "d"}},
"leading sep": {input: "沙河有沙又有河", sep: "沙", want: []string{"河有", "又有河"}},
}
for name, tc := range tests {
// 使用t.Run()执行子测试
t.Run(name, func(t *testing.T) {
got := strings.Split(tc.input, tc.sep)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("expected:%#v, got:%#v", tc.want, got)
}
})
}
}
go test -v
=== RUN TestStringSplit
--- PASS: TestStringSplit (0.00s)
=== RUN TestStringSplit1
=== RUN TestStringSplit1/wrong_sep
=== RUN TestStringSplit1/more_sep
=== RUN TestStringSplit1/leading_sep
string_test.go:53: expected:[]string{"河有", "又有河"}, got:[]string{"", "河有", "又有河"}
=== RUN TestStringSplit1/simple
--- FAIL: TestStringSplit1 (0.00s)
--- PASS: TestStringSplit1/wrong_sep (0.00s)
--- PASS: TestStringSplit1/more_sep (0.00s)
--- FAIL: TestStringSplit1/leading_sep (0.00s)
--- PASS: TestStringSplit1/simple (0.00s)
FAIL
exit status 1
FAIL 07-UnitTestBenchmarkTest/UnitTest 0.629s
如果指向执行指定的测试函数,可以通过 -run
参数指定,它对应一个正则表达式,只有函数名匹配的上的测试函数才会被 go test
命令执行。
go test -v -run=Split/simple
=== RUN TestStringSplit
--- PASS: TestStringSplit (0.00s)
=== RUN TestStringSplit1
=== RUN TestStringSplit1/simple
--- PASS: TestStringSplit1 (0.00s)
--- PASS: TestStringSplit1/simple (0.00s)
PASS
ok 07-UnitTestBenchmarkTest/UnitTest 0.046s
测试覆盖率是你的代码被测试套件覆盖的百分比。通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。
Go提供内置功能来检查你的代码覆盖率。我们可以使用go test -cover
来查看测试覆盖率。Go还提供了一个额外的-coverprofile
参数,用来将覆盖率相关的记录信息输出到一个文件。
使用:go test -cover -coverprofile="a.out"
,将会生成 a.out
文件。然后可以执行go tool cover -html="a.out"
,使用cover
工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成一个HTML
报告。
func BenchmarkFuncName(b *testing.B) {}
Benchmark
开头*testing.B
类型for
循环,循环次数是 b.N
次(b.N
的值是系统根据实际情况调整,保证测试稳定性)下述创建了一个测试函数:
func split(s, sep string) (result []string) {
result = make([]string, 0, strings.Count(s, sep)+1)
i := strings.Index(s, sep)
for i > -1 {
result = append(result, s[:i])
s = s[i+len(sep):]
i = strings.Index(s, sep)
}
result = append(result, s)
return
}
// BenchmarkSplit 测试自定义的 split 函数
func BenchmarkSplit(b *testing.B) {
for i := 0; i < b.N; i++ {
split("沙河有沙又有河", "沙")
}
}
使用 go test -v -bench=Split
命令执行基准测试,基础测试默认不会执行,必须携带 -bench
参数。
go test -v -bench=Split
goos: windows
goarch: amd64
pkg: 07-UnitTestBenchmarkTest/BenchmarkTest
cpu: 13th Gen Intel(R) Core(TM) i9-13900HX
BenchmarkSplit
BenchmarkSplit-32 26486323 47.05 ns/op
PASS
ok 07-UnitTestBenchmarkTest/BenchmarkTest 1.897s
其中 BenchmarkSplit-32
表示对 Split 函数进行基准测试,数字 32
表示 GOMAXPROCS
的值,26486323
和 47.05 ns/op
表示每次调用 Split 函数耗时 47.05ns
,这个结果是 26486323
次调用的平均值。
还可以为基准测试添加 -benchmem
参数,来获得内存分配的统计数据。如下所示,48 B/op
表示每次操作内存分配了48字节,1 allocs/op
表示每次操作进行了1次内存分配。
go test -v -bench=Split -benchmem
goos: windows
goarch: amd64
pkg: 07-UnitTestBenchmarkTest/BenchmarkTest
cpu: 13th Gen Intel(R) Core(TM) i9-13900HX
BenchmarkSplit
BenchmarkSplit-32 26387544 45.29 ns/op 48 B/op 1 allocs/op
PASS
ok 07-UnitTestBenchmarkTest/BenchmarkTest 1.295s
性能比较函数通常是一个带有参数的函数,被多个不同的 Benchmark
函数传入不同的值来调用。
func fib(n int) int {
if n < 2 {
return n
}
return fib(n-1) + fib(n-2)
}
func benchmarkFib(b *testing.B, n int) {
for i := 0; i < b.N; i++ {
fib(n)
}
}
func BenchmarkFib1(b *testing.B) { benchmarkFib(b, 1) }
func BenchmarkFib2(b *testing.B) { benchmarkFib(b, 2) }
func BenchmarkFib3(b *testing.B) { benchmarkFib(b, 3) }
func BenchmarkFib10(b *testing.B) { benchmarkFib(b, 10) }
func BenchmarkFib20(b *testing.B) { benchmarkFib(b, 20) }
func BenchmarkFib40(b *testing.B) { benchmarkFib(b, 40) }
上述编写了一个斐波那契数列计算函数,并在不同大小的n值的情况下进行基准测试。
go test -v -bench=Fib
goos: windows
goarch: amd64
pkg: 07-UnitTestBenchmarkTest/BenchmarkTest
cpu: 13th Gen Intel(R) Core(TM) i9-13900HX
BenchmarkFib1
BenchmarkFib1-32 1000000000 0.5479 ns/op
BenchmarkFib2
BenchmarkFib2-32 736555106 1.600 ns/op
BenchmarkFib3
BenchmarkFib3-32 459925009 2.590 ns/op
BenchmarkFib10
BenchmarkFib10-32 7295648 164.5 ns/op
BenchmarkFib20
BenchmarkFib20-32 59763 20160 ns/op
BenchmarkFib40
BenchmarkFib40-32 4 306910725 ns/op
PASS
ok 07-UnitTestBenchmarkTest/BenchmarkTest 8.692s
默认情况下,每个基准测试至少运行1秒,可以通过参数 -benchtime
设置基准测试时间。此外还有 -cpu
参数设置运行基准测试的并发数,-count
参数设置运行测试的总次数。
go test -v -bench="." -benchtime=2s
goos: windows
goarch: amd64
pkg: 07-UnitTestBenchmarkTest/BenchmarkTest
cpu: 13th Gen Intel(R) Core(TM) i9-13900HX
BenchmarkSplit
BenchmarkSplit-32 48237133 44.58 ns/op
BenchmarkFib1
BenchmarkFib1-32 1000000000 0.5392 ns/op
BenchmarkFib2
BenchmarkFib2-32 1000000000 1.575 ns/op
BenchmarkFib3
BenchmarkFib3-32 931792406 2.590 ns/op
BenchmarkFib10
BenchmarkFib10-32 14704368 163.8 ns/op
BenchmarkFib20
BenchmarkFib20-32 119018 20080 ns/op
BenchmarkFib40
BenchmarkFib40-32 7 303590871 ns/op
PASS
ok 07-UnitTestBenchmarkTest/BenchmarkTest 14.857s
b.ResetTimer
之前的处理不会放到执行时间里,也不会输出到报告中,所以可以在之前做一些不计划作为测试报告的操作。b.ReportAllocs()
记录内存分配数据。
func BenchmarkResetTimer(b *testing.B) {
time.Sleep(3 * time.Second)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
split("abc.poe.org.com", ".")
}
}
go test -bench="Timer"
goos: windows
goarch: amd64
pkg: 07-UnitTestBenchmarkTest/BenchmarkTest
cpu: 13th Gen Intel(R) Core(TM) i9-13900HX
BenchmarkResetTimer-32 32262052 34.99 ns/op 64 B/op 1 allocs/op
PASS
ok 07-UnitTestBenchmarkTest/BenchmarkTest 16.843s
func (b *B) RunParallel(body func(*PB))
会以并行的方式执行给定的基准测试。
RunParallel
会创建出多个 goroutine
,并将 b.N
分配给这些 goroutine
执行, 其中 goroutine
数量的默认值为GOMAXPROCS
。用户如果想要增加非 CPU
受限(non-CPU-bound)
基准测试的并行性, 那么可以在 RunParallel
之前调用 SetParallelism
。RunParallel
通常会与 -cpu
标志一同使用。
func BenchmarkSplitParallel(b *testing.B) {
// b.SetParallelism(1) // 设置使用的cpu数
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
split("cn.com.org", ".")
}
})
}
go test -bench="Parallel" -cpu=4
goos: windows
goarch: amd64
pkg: 07-UnitTestBenchmarkTest/BenchmarkTest
cpu: 13th Gen Intel(R) Core(TM) i9-13900HX
BenchmarkSplitParallel-4 95221719 14.72 ns/op
PASS
ok 07-UnitTestBenchmarkTest/BenchmarkTest 2.329s
参考:https://www.liwenzhou.com/posts/Go/unit-test/