Go:单元测试
目录:
go test工具
Go中测试依赖go test
命令,测试的代码就是Go代码,在包内以_test.go结尾的文件就是测试代码,不会被编译到可执行文件中
*_test.go
有三种类型的函数
类型 | 格式 | 作用 |
---|---|---|
测试函数 | 函数名前缀为Test | 测试程序的逻辑是否正确 |
基准函数 | 函数名前缀为Benchmark | 测试函数的性能 |
示例函数 | 函数名前缀为Example | 为文档提供示例文档 |
go test
会遍历所有符合*_test.go
规则的文件中符合上述命名规则的函数,然后生成一个临时的main包用于调试测试函数,然后构建并运行,报告测试结果,最后清理测试中生成的临时文件
测试函数
测试函数的格式
测试函数需要导入testing包
func TestName(t *testing.T) {
...
}
参数t用于报告测试失败和附加的日志信息,拥有方法有
func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
func (c *T) Log(args ...interface{})
func (c *T) Logf(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
测试函数示例
先定义一个split包和一个Split函数
split/split.go
package split
import "strings"
func Split(str string, sep string) (ret []string) {
index := strings.Index(str, sep)
for index >= 0 {
ret = append(ret, str[:index])
str = str[index+1:]
index = strings.Index(str, sep)
}
ret = append(ret, str)
return
}
split/split_test.go
package split
import (
"reflect"
"testing"
)
// 测试函数名必须以Test开头,必须接收一个*testing.T类型参数
func TestSplit(t *testing.T) {
got := Split("a:b:c", ":") // 程序输出的结果
want := []string{"a", "b", "c"} // 期望的结果
// 因为slice不能比较直接,借助反射包中的方法比较
if !reflect.DeepEqual(want, got) {
// 测试失败输出错误提示
t.Errorf("excepted:%v, got:%v", want, got)
}
}
进行测试
$ go test
PASS
ok split 0.002s
如果测试失败可以将want修改,例如改为[]string{"a", "b", "cd"}
$ go test
--- FAIL: TestSplit (0.00s)
split_test.go:15: excepted:[a b cd], got:[a b c]
FAIL
exit status 1
FAIL split 0.002s
可以再增加一个测试
package split
import (
"reflect"
"testing"
)
// 测试函数名必须以Test开头,必须接收一个*testing.T类型参数
func TestSplit(t *testing.T) {
got := Split("a:b:c", ":") // 程序输出的结果
want := []string{"a", "b", "c"} // 期望的结果
// 因为slice不能比较直接,借助反射包中的方法比较
if !reflect.DeepEqual(want, got) {
// 测试失败输出错误提示
t.Errorf("excepted:%v, got:%v", want, got)
}
}
func TestMoreSplit(t *testing.T) {
got := Split("a-==-b-==-c", "-==-") // 程序输出的结果
want := []string{"a", "b", "c"} // 期望的结果
// 因为slice不能比较直接,借助反射包中的方法比较
if !reflect.DeepEqual(want, got) {
// 测试失败输出错误提示
t.Errorf("excepted:%v, got:%v", want, got)
}
}
打印结果
$ go test
--- FAIL: TestMoreSplit (0.00s)
split_test.go:25: excepted:[a b c], got:[a ==-b ==-c]
FAIL
exit status 1
FAIL split 0.001s
使用-v
参数,查看测试函数名称和运行时间
$ go test -v
=== RUN TestSplit
--- PASS: TestSplit (0.00s)
=== RUN TestMoreSplit
--- FAIL: TestMoreSplit (0.00s)
split_test.go:25: excepted:[a b c], got:[a ==-b ==-c]
FAIL
exit status 1
FAIL split 0.002s
这样这个分隔符如果是一个非1长度的字符串就失效了,修复的时候把1换成len(sep)
就好了
测试组
测试组就是通过struct来构建一组测试参数和期望结果,通过循环的方式进行单元测试
package split
import (
"reflect"
"testing"
)
func TestGroupSplit(t *testing.T) {
type test struct {
str string
sep string
want []string
}
tests := []test{
{str: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
{str: "a:b:c", sep: ",", want: []string{"a:b:c"}},
{str: "abcd", sep: "bc", want: []string{"a", "d"}},
{str: "一二一一二三四", sep: "二", want: []string{"一", "一一", "三四"}},
}
// 遍历切片,逐一执行测试用例
for _, tc := range tests {
got := Split(tc.str, tc.sep)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("excepted:%v, got:%v", tc.want, got)
}
}
}
子测试
因为在测试中可能不能正确的发现哪个用例失败了,将测试组的splice改为map
=== RUN TestGroupSplit
=== RUN TestGroupSplit/simple
=== RUN TestGroupSplit/wrong_sep
=== RUN TestGroupSplit/more_sep
=== RUN TestGroupSplit/leading_sep
--- PASS: TestGroupSplit (0.00s)
--- PASS: TestGroupSplit/simple (0.00s)
--- PASS: TestGroupSplit/wrong_sep (0.00s)
--- PASS: TestGroupSplit/more_sep (0.00s)
--- PASS: TestGroupSplit/leading_sep (0.00s)
PASS
ok split 0.002s
执行结果
=== RUN TestGroupSplit
=== RUN TestGroupSplit/simple
=== RUN TestGroupSplit/wrong_sep
=== RUN TestGroupSplit/more_sep
=== RUN TestGroupSplit/leading_sep
--- PASS: TestGroupSplit (0.00s)
--- PASS: TestGroupSplit/simple (0.00s)
--- PASS: TestGroupSplit/wrong_sep (0.00s)
--- PASS: TestGroupSplit/more_sep (0.00s)
--- PASS: TestGroupSplit/leading_sep (0.00s)
PASS
ok split 0.002s
测试覆盖率
$ go test -cover
PASS
coverage: 100.0% of statements
ok split 0.002s
可以将详细的情况输出到文件进而转化为html
$ go test -cover -coverprofile=c.out
PASS
coverage: 100.0% of statements
ok split 0.002s
$ cat c.out
mode: set
split/split.go:5.51,7.20 2 1
split/split.go:12.5,13.11 2 1
split/split.go:7.20,11.6 3 1
$ go tool cover -html=c.out
HTML output written to /tmp/cover955594976/coverage.html
就是这个样子
基准测试
基准测试时在一定的工作负载下检测程序性能的一种方法
func BenchmarkName(b *testing.B){
// ...
}
基准测试是以Benchmark为前缀,需要一个*testing.B
类型的参数b,基准测试必须执行b.N次,测试才有对照性,b.N的值是系统根据实际情况下进行调整的,从而保障测试的稳定性
testing.B
有以下方法
func (c *B) Error(args ...interface{})
func (c *B) Errorf(format string, args ...interface{})
func (c *B) Fail()
func (c *B) FailNow()
func (c *B) Failed() bool
func (c *B) Fatal(args ...interface{})
func (c *B) Fatalf(format string, args ...interface{})
func (c *B) Log(args ...interface{})
func (c *B) Logf(format string, args ...interface{})
func (c *B) Name() string
func (b *B) ReportAllocs()
func (b *B) ResetTimer()
func (b *B) Run(name string, f func(b *B)) bool
func (b *B) RunParallel(body func(*PB))
func (b *B) SetBytes(n int64)
func (b *B) SetParallelism(p int)
func (c *B) Skip(args ...interface{})
func (c *B) SkipNow()
func (c *B) Skipf(format string, args ...interface{})
func (c *B) Skipped() bool
func (b *B) StartTimer()
func (b *B) StopTimer()
基准测试示例
示例基准测试代码
package split
import (
"reflect"
"testing"
)
func BenchmarkSplit(b *testing.B) {
for i := 0; i < b.N; i++ {
Split("一二一一二三四", "二")
}
}
执行测试命令,需要增加-bench参数
$ go test -bench=Split
goos: linux
goarch: amd64
pkg: split
BenchmarkSplit 5000000 348 ns/op
PASS
ok split 2.079s
调用了5000000次,平均调用时长为348ns
可以获取到内存分配的统计数据
$ go test -bench=Split -benchmem
goos: linux
goarch: amd64
pkg: split
BenchmarkSplit 5000000 346 ns/op 112 B/op 3 allocs/op
PASS
ok split 2.079s
112B代表每次操作内存分配了112字节,3代表每次操作进行了3次内存分配
修改为提前申请好内存
package split
import "strings"
func Split(str string, sep string) (ret []string) {
ret = make([]string, 0, strings.Count(str, sep)+1)
index := strings.Index(str, sep)
for index >= 0 {
ret = append(ret, str[:index])
str = str[index+len(sep):]
index = strings.Index(str, sep)
}
ret = append(ret, str)
return
}
再进行一次基准测试
$ go test -bench=Split -benchmem
goos: linux
goarch: amd64
pkg: split
BenchmarkSplit 10000000 215 ns/op 48 B/op 1 allocs/op
PASS
ok split 2.370s
性能比较函数
之前的基准测试,只能测试给定操作的绝对值,但是对于绝多数性能问题,是发生在两个不同操作之间的相对耗时,比如一个同一个函数处理1000个元素和处理1万甚至100万的耗时差距是多少?
示例性能比较函数
func benchmark(b *testing.B, size int){/* ... */}
func Benchmark10(b *testing.B){ benchmark(b, 10) }
func Benchmark100(b *testing.B){ benchmark(b, 100) }
func Benchmark1000(b *testing.B){ benchmark(b, 1000) }
测试一个斐波那契数列的函数
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) }
测试结果
$ go test -bench=.
goos: linux
goarch: amd64
pkg: split
BenchmarkFib1 1000000000 2.92 ns/op
BenchmarkFib2 200000000 8.53 ns/op
BenchmarkFib3 100000000 13.1 ns/op
BenchmarkFib10 3000000 557 ns/op
BenchmarkFib20 30000 59637 ns/op
BenchmarkFib40 1 1155978074 ns/op
PASS
ok split 13.960s
看到40的时候只执行了一次,可以调高基准测试时间
$ go test -bench=Fib40 -benchtime=20s
goos: linux
goarch: amd64
pkg: split
BenchmarkFib40 30 954169460 ns/op
PASS
ok split 29.514s
性能测试的错误问题
b.N是不能作为大小进行输入
// 错误示范1
func BenchmarkFibWrong(b *testing.B) {
for n := 0; n < b.N; n++ {
Fib(n)
}
}
// 错误示范2
func BenchmarkFibWrong2(b *testing.B) {
Fib(b.N)
}
重置时间
b.ResetTimer
之前的处理不会放到执行时间里,也不会输出到报告中,所以可以在之前做一些不计划作为测试报告的操作
func BenchmarkSplit(b *testing.B) {
time.Sleep(5 * time.Second) // 假设需要做一些耗时的无关操作
b.ResetTimer() // 重置计时器
for i := 0; i < b.N; i++ {
Split("一二一一二三四", "二")
}
}
并行测试
func (b *B) RunParallel(body func(*PB))
RunParallel会以并行的方式执行给定的基准测试
RunParallel会创建出多个goroutine,并将b.N分配给goroutine执行,其中goroutine数量为GOMAXPROCS,如果想要不受CPU影响,可以在RunParallel之前调用SetParallelism
func BenchmarkSplitParallel(b *testing.B) {
// b.SetParallelism(1) // 设置使用的CPU数
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Split("一二一一二三四", "二")
}
})
}
Setup与TearDown
通过在*_test.go
文件中定义TestMain函数来可以在测试之前进行额外的设置(setup)或在测试之后进行拆卸(teardown)操作。
TestMain
如果测试文件包含函数func TestMain(m *testing.M)
那么生成的测试会先调用TestMain(m),然后再运行具体测试。
TestMain运行在主goroutine中, 可以在调用m.Run
前后做任何设置(setup)和拆卸(teardown)。退出测试的时候应该使用m.Run的返回值作为参数调用os.Exit
。
一个使用TestMain来设置Setup和TearDown的示例如下:
func TestMain(m *testing.M) {
fmt.Println("write setup code here...") // 测试之前的做一些设置
// 如果 TestMain 使用了 flags,这里应该加上flag.Parse()
retCode := m.Run() // 执行测试
fmt.Println("write teardown code here...") // 测试之后做一些拆卸工作
os.Exit(retCode) // 退出测试
}
需要注意的是
在调用TestMain时, flag.Parse
并没有被调用。所以如果TestMain依赖于command-line标志(包括 testing 包的标记), 则应该显示的调用flag.Parse。
子测试的Setup与Teardown
有时候我们可能需要为每个测试集设置Setup与Teardown,也有可能需要为每个子测试设置Setup与Teardown。下面我们定义两个函数工具函数如下:
// 测试集的Setup与Teardown
func setupTestCase(t *testing.T) func(t *testing.T) {
t.Log("如有需要在此执行:测试之前的setup")
return func(t *testing.T) {
t.Log("如有需要在此执行:测试之后的teardown")
}
}
// 子测试的Setup与Teardown
func setupSubTest(t *testing.T) func(t *testing.T) {
t.Log("如有需要在此执行:子测试之前的setup")
return func(t *testing.T) {
t.Log("如有需要在此执行:子测试之后的teardown")
}
}
使用方式
func TestSplit(t *testing.T) {
type test struct { // 定义test结构体
input string
sep string
want []string
}
tests := map[string]test{ // 测试用例使用map存储
"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{"一", "一一", "三四"}},
}
teardownTestCase := setupTestCase(t) // 测试之前执行setup操作
defer teardownTestCase(t) // 测试之后执行testdoen操作
for name, tc := range tests {
t.Run(name, func(t *testing.T) { // 使用t.Run()执行子测试
teardownSubTest := setupSubTest(t) // 子测试之前执行setup操作
defer teardownSubTest(t) // 测试之后执行testdoen操作
got := Split(tc.input, tc.sep)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("excepted:%#v, got:%#v", tc.want, got)
}
})
}
}
测试结果如下:
$ go test -v
=== RUN TestSplit
=== RUN TestSplit/simple
=== RUN TestSplit/wrong_sep
=== RUN TestSplit/more_sep
=== RUN TestSplit/leading_sep
--- PASS: TestSplit (0.00s)
split_test.go:71: 如有需要在此执行:测试之前的setup
--- PASS: TestSplit/simple (0.00s)
split_test.go:79: 如有需要在此执行:子测试之前的setup
split_test.go:81: 如有需要在此执行:子测试之后的teardown
--- PASS: TestSplit/wrong_sep (0.00s)
split_test.go:79: 如有需要在此执行:子测试之前的setup
split_test.go:81: 如有需要在此执行:子测试之后的teardown
--- PASS: TestSplit/more_sep (0.00s)
split_test.go:79: 如有需要在此执行:子测试之前的setup
split_test.go:81: 如有需要在此执行:子测试之后的teardown
--- PASS: TestSplit/leading_sep (0.00s)
split_test.go:79: 如有需要在此执行:子测试之前的setup
split_test.go:81: 如有需要在此执行:子测试之后的teardown
split_test.go:73: 如有需要在此执行:测试之后的teardown
=== RUN ExampleSplit
--- PASS: ExampleSplit (0.00s)
PASS
ok split 0.006s
示例函数
示例函数的主要用途是为Split函数编写一个示例
func ExampleSplit() {
fmt.Println(split.Split("a:b:c", ":"))
fmt.Println(split.Split("一二一一二三四", "二"))
// Output:
// [a b c]
// [一 一一 三四]
}
用途有
- 作为文档使用
- 包含了
// Output:
可以通过go test进行测试 - 可以在golang.org的godoc文档服务器上使用
Go Playground
运行示例代码