1. go test
go test 命令是一个按照一定约定和组织的测试代码的驱动程序,它会遍历当前包目录内的所有名称符合 *_test.go 的源文件,找出其中命名规范符合以下之一的函数,然后生成一个临时的 main 包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。
- 测试函数:函数名前缀为 Test,用来测试程序的一些逻辑行为是否正确
- 基准函数:函数名前缀为 Benchmark,用来测试函数的性能
- 示例函数:函数名前缀为 Example,为文档提供示例文档
命令格式:go test [-c] [-i] [build flags] [packages] [flags for test binary]
go test 命令参数如下:
-c 编译成可执行的二进制文件,但是不运行测试
-i 安装测试包依赖的package,但是不运行测试
-v 是否输出全部的单元测试用例,默认没有加上,所以只输出失败的单元测试用例
-run=pattern 只跑哪些单元测试用例
-bench=patten 只跑哪些性能测试用例
-benchmem 是否在性能测试的时候输出内存情况
-benchtime=t 性能测试运行的时间,默认是1s
-cpuprofile=cpu.out 是否输出cpu性能分析文件
-memprofile=mem.out 是否输出内存性能分析文件
-blockprofile=block.out 是否输出内部goroutine阻塞的性能分析文件
-memprofilerate=n 内存性能分析打点的内存分配间隔
-blockprofilerate=n goroutine阻塞时候打点的纳秒数
-parallel=n 性能测试的程序并行cpu数,默认等于GOMAXPROCS
-timeout=t 如果测试用例运行时间超过t,则抛出panic
-cpu=1,2,4 程序运行在哪些CPU上面,使用二进制的1所在位代表
-short 将那些运行时间较长的测试用例运行时间缩短
-failfast 失败后不继续后面的测试
2. 单元测试
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证,通过测试函数和方法等小段代码,使得我们可以及早发现程序错误,保证了代码的正确性。
2.1 测试函数
我们可以对已有的源文件,在同一目录下,增加一个文件名后追加 _test 的新源文件,用来编写测试函数,例子如下:
// 已有源文件
tool.go
// 单元测试源文件
tool_test.go
测试函数必须导入 testing 包,函数名必须以 Test 开头,后面名称以大写字母或下划线开头,一般针对某个函数的测试函数可以在原函数名称前加 Test 前缀。测试函数的参数必须为 t *testing.T。
// 原函数
func Concat() {}
// 测试函数
func TestConcat(t *testing.T) {
// ...
}
testing 包文档:https://pkg.go.dev/testing
参数 t 可以用于在测试函数中报告测试失败和附加的日志信息,包含方法:
// 标记测试失败,继续执行
func (c *T) Fail()
// 标记测试失败,中止退出
func (c *T) FailNow()
// 测试是否失败
func (c *T) Failed() bool
// 打印日志
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
// 打印日志,标记测试失败,继续执行
func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
// 打印日志,标记测试失败,中止退出
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
// 单元测试的名称
func (c *T) Name() string
// 并发执行
func (t *T) Parallel()
// 执行子测试
func (t *T) Run(name string, f func(t *T)) bool
// 跳过测试
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
// 测试是否跳过
func (c *T) Skipped() bool
2.2 示例
以已有的函数为例:
// src/tool/tool.go
package tool
// Concat 拼接字符串
func Concat(strs ...string) string {
str := ""
for _, s := range strs {
str += s
}
return str
}
编写测试函数:
// src/tool/tool_test.go
package tool
import (
"testing"
)
func TestConcat(t *testing.T) {
strs := []string{"a", "b", "c"}
str := Concat(strs...)
expect := "abc"
if str != expect {
t.Errorf("expect: %s, got: %s", expect, str)
}
}
在命令行去到当前包路径,执行测试命令:
$ cd src/tool/
$ go test
PASS
ok go-test/src/tool 0.301s
# 也可以通过相对路径指定测试路径
$ go test ./src/tool/
尝试增加一个错误的测试函数:
func TestConcatFail(t *testing.T) {
strs := []string{"1", "2"}
str := Concat(strs...)
expect := "123"
if str != expect {
t.Errorf("expect: %s, got: %s", expect, str)
}
}
执行测试命令,将会执行所有测试函数,只能在全部执行成功时展示成功,否则展示出错的那个报错,信息如下:
$ go test
--- FAIL: TestConcatFail (0.00s)
tool_test.go:21: expect: 123, got: 12
FAIL
exit status 1
FAIL go-test/src/tool 0.169s
-v 参数可以分别输出每个测试函数的执行情况。
$ go test -v
=== RUN TestConcat
--- PASS: TestConcat (0.00s)
=== RUN TestConcatFail
tool_test.go:21: expect: 123, got: 12
--- FAIL: TestConcatFail (0.00s)
FAIL
exit status 1
FAIL go-test/src/tool 0.267s
-run 参数可以指定要执行的测试函数,通过正则表达式匹配测试函数名称。
$ go test -v -run=Fail
=== RUN TestConcatFail
tool_test.go:21: expect: 123, got: 12
--- FAIL: TestConcatFail (0.00s)
FAIL
exit status 1
FAIL go-test/src/tool 0.182s
2.3 测试组
可以讲多组测试用例合并到一起,放到一个测试函数内进行测试。
// src/tool/tool_test.go
package tool
import (
"testing"
)
func TestGroupConcat(t *testing.T) {
tests := []struct {
strs []string
want string
}{
{[]string{"a", "b", "c"}, "abc"},
{[]string{"11 2", "3"}, "1 23"},
{[]string{}, ""},
}
for _, test := range tests {
if got := Concat(test.strs...); got != test.want {
t.Errorf("expect: %s, got: %s", test.want, got)
}
}
}
如果执行出错,会展示具体哪个测试用例报错。
$ go test -v
=== RUN TestGroupConcat
tool_test.go:18: expect: 1 23, got: 11 23
--- FAIL: TestGroupConcat (0.00s)
FAIL
exit status 1
FAIL go-test/src/tool 0.265s
2.4 子测试
当测试用例较多时,可以在测试函数内用 t.Run 方法执行子函数,
// src/tool/tool_test.go
package tool
import (
"testing"
)
func TestSubConcat(t *testing.T) {
type args struct {
strs []string
}
tests := []struct {
name string
args args
want string
}{
{"alphabet", args{strs: []string{"a", "b", "c"}}, "abc"},
{"number", args{strs: []string{"11 2", "3"}}, "1 23"},
{"empty", args{strs: []string{}}, ""},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := Concat(test.args.strs...); got != test.want {
t.Errorf("expect: %s, got: %s", test.want, got)
}
})
}
}
如果执行出错,会展示具体出错的测试用例名称以及报错。
$ go test -v
=== RUN TestSubConcat
=== RUN TestSubConcat/alphabet
=== RUN TestSubConcat/number
tool_test.go:23: expect: 1 23, got: 11 23
=== RUN TestSubConcat/empty
--- FAIL: TestSubConcat (0.00s)
--- PASS: TestSubConcat/alphabet (0.00s)
--- FAIL: TestSubConcat/number (0.00s)
--- PASS: TestSubConcat/empty (0.00s)
FAIL
exit status 1
FAIL go-test/src/tool 0.253s
可以通过 -run 参数以正则表达式的形式指定子测试。
$ go test -v -run=TestSubConcat/alphabet
=== RUN TestSubConcat
=== RUN TestSubConcat/alphabet
--- PASS: TestSubConcat (0.00s)
--- PASS: TestSubConcat/alphabet (0.00s)
PASS
ok go-test/src/tool 0.193s
2.5 测试覆盖率
-cover 参数可以查看测试覆盖率,即当前包的所有代码中,在单元测试函数中被至少运行到一次的代码,占全部代码的比例。
执行后,go test 会先走一遍所有的测试函数,然后再统计出测试覆盖率。
$ go test -cover
PASS
go-test/src/tool coverage: 100.0% of statements
ok go-test/src/tool 0.228s
Go 还提供了一个额外的 -coverprofile 参数,用来将覆盖率相关的记录信息输出到一个文件。然后再以 -func 参数来查看每个函数的覆盖率。
go test -cover -coverprofile=cover.out -covermode=count
go tool cover -func=cover.out
3. 基准测试
基准测试是在一定的工作负载之下检测程序性能的一种方法。
3.1 基准测试函数
基准测试函数必须导入 testing 包,函数名必须以 Benchmark 开头,后面名称以大写字母或下划线开头,一般针对某个函数的测试函数可以在原函数名称前加 Benchmark 前缀。基准测试函数的参数必须为 b *testing.B,基准测试必须包含一个 for 循环且要执行 b.N 次。
// 原函数
func Concat() {}
// 基准测试函数
func BenchmarkConcat(b *testing.B) {
// ...
}
参数 b 可以用于在测试函数中报告测试失败和附加的日志信息,包含方法:
// 标记测试失败,继续执行
func (c *B) Fail()
// 标记测试失败,中止退出
func (c *B) FailNow()
// 测试是否失败
func (c *B) Failed() bool
// 打印日志
func (c *B) Log(args ...interface{})
func (c *B) Logf(format string, args ...interface{})
// 打印日志,标记测试失败,继续执行
func (c *B) Error(args ...interface{})
func (c *B) Errorf(format string, args ...interface{})
// 打印日志,标记测试失败,中止退出
func (c *B) Fatal(args ...interface{})
func (c *B) Fatalf(format string, args ...interface{})
// 单元测试的名称
func (c *B) Name() string
// 执行子测试
func (b *B) Run(name string, f func(b *B)) bool
// 并发执行
func (b *B) RunParallel(body func(*PB))
// 跳过测试
func (c *B) Skip(args ...interface{})
func (c *B) SkipNow()
func (c *B) Skipf(format string, args ...interface{})
// 测试是否跳过
func (c *B) Skipped() bool
3.2 示例
还是以上面单元测试的 Concat 函数为例:
// src/tool/tool.go
package tool
// Concat 拼接字符串
func Concat(strs ...string) string {
str := ""
for _, s := range strs {
str += s
}
return str
}
编写基准测试函数:
package tool
import (
"testing"
)
func BenchmarkConcat(b *testing.B) {
strs := []string{"a", "b", "c"}
for i := 0; i < b.N; i++ {
Concat(strs...)
}
}
执行测试命令:
$ cd src/tool/
$ go test -bench=Concat -benchmem
goos: darwin
goarch: amd64
pkg: go-test/src/tool
cpu: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHz
BenchmarkConcat-8 19056243 59.41 ns/op 6 B/op 2 allocs/op
PASS
ok go-test/src/tool 1.412s
-benchmem 参数可以得到内存分配的统计数据。
这里的 BenchmarkConcat-8 表示 GOMAXPROCS = 19056243 表示调用 Concat 函数的次数,59.41 ns/op 表示每次调用的平均耗时,6 B/op 表示每次调用分配的内存大小,2 allocs/op 表示每次调用进行了多少次内存分配。
3.3 重置时间
在进行基准测试的开头如果需要做一些其他工作,但是又不想统计进时间统计内,可以先重置一下计时器。
func BenchmarkConcat(b *testing.B) {
// do sth.
b.ResetTimer() // 重置计时器
strs := []string{"a", "b", "c"}
for i := 0; i < b.N; i++ {
Concat(strs...)
}
}
4. 示例函数
示例函数以 Example 作为前缀,它们没有参数和返回值。
func ExampleConcat() {
strs := []string{"a", "b", "c"}
expect := "abc"
got := Concat(strs...)
fmt.Printf("expect: %s, got: %s", expect, got)
}
示例函数能够作为文档直接使用,例如基于web的godoc中能把示例函数与对应的函数或包相关联。