推荐使用TDD(Test Driver Development),然而现在很多人都不注重测试。
go test工具 Go语言中依赖go test
命令。编写测试代码和编写普通的Go代码过程是类似的。
go test
命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go
为后缀的源代码文件都是go test
测试的一部分,不会被go build
编译到最终可执行文件。
在*_go test
文件中有三种类型的函数,单元测试函数,基准测试函数和实力函数。
类型
格式
作用
测试函数
函数名前缀为Test
测试程序的一些逻辑行为是否正确
基准函数
函数名前缀为Benchmark
测试函数的性能
示例函数
函数名前缀为Example
为文档提供示例文档
go test
命令会遍历所有的*_test.go
文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行,报告测试结果,最后清理测试中生成的临时文件。
测试函数 测试函数的格式 每个测试函数必须导入testing
包,测试函数的基本格式(签名)如下。
1 2 3 func TestName (t *testing.T) { }
测试函数的名字开头必须以Test
开头,可选的后缀名必须以大写字母开头。
1 2 3 func TestAdd (t *testing.T) {...}func TestSum (t *testing.T) {...}func TestLog (t *testing.T) {...}
其中参数t
用于报告测试失败和附加的日志信息。testing.T
拥有的方法如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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
函数。
1 2 3 4 5 6 7 8 9 10 11 func Split (s, sep string ) (result []string ) { i := strings.Index(s, sep) for i > -1 { result = append (result, s[:i]) s = s[i+1 :] i = strings.Index(s, sep) } result = append (result, s) return }
在当前目录下创建一个split_test.go
的测试文件,并定义一个测试函数:
1 2 3 4 5 6 7 8 9 10 11 12 func TestSplit (t *testing.T) { got := Split("a:b:c" , ":" ) want := []string {"a" , "b" ,"c" } if !reflect.DeepEqual(want, got) { t.Errorf("excepted:%v, got:%v" , want, got) } }
在split
包路径下执行go test
命令,得到输出结果:
1 2 3 05split>go test PASS ok github.com/DurianLollipop/StudyGo/day08/05split 0.623s
创建更多的测试测试字符串切割函数:
1 2 3 4 5 6 7 func TestSplit2 (t *testing.T) { got := Split("abcd" , "bc" ) want := []string {"a" , "d" } if !reflect.DeepEqual(want, got) { t.Errorf("excepted:%v, got:%v" , want, got) } }
再次运行go test
命令,输出结果如下:
1 2 3 4 5 6 05split>go test --- FAIL: TestSplit2 (0.00s) split_test.go:24: excepted:[a d], got:[a cd ] FAIL exit status 1FAIL github.com/DurianLollipop/StudyGo/day08/05split 0.546s
使用go test
命令添加-v
参数,查看测试函数名称和运行时间,可以清楚的看到所有测试执行的测试的结果:
1 2 3 4 5 6 7 8 9 05split>go test -v === RUN TestSplit --- PASS: TestSplit (0.00s) === RUN TestSplit2 TestSplit2: split_test.go:24: excepted:[a d], got:[a cd ] --- FAIL: TestSplit2 (0.00s) FAIL exit status 1FAIL github.com/DurianLollipop/StudyGo/day08/05split 0.506s
在go test
命令后添加-run
参数,它对应一个正则表达式,只有函数名称匹配上的测试函数才会被go test
命令执行。
1 2 3 4 5 6 7 05split>go test -run="TestSplit2" -v === RUN TestSplit2 TestSplit2: split_test.go:24: excepted:[a d], got:[a cd ] --- FAIL: TestSplit2 (0.00s) FAIL exit status 1FAIL github.com/DurianLollipop/StudyGo/day08/05split 0.517s
针对程序中出现的问题进行修复:
1 2 3 4 5 6 7 8 9 10 11 12 func Split (s, sep string ) (result []string ) { 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 }
在修复完测试之后再执行一遍涉及到的所有测试,以确保没有引入新的问题。
1 2 3 4 5 6 7 05split>go test -v === RUN TestSplit --- PASS: TestSplit (0.00s) === RUN TestSplit2 --- PASS: TestSplit2 (0.00s) PASS ok github.com/DurianLollipop/StudyGo/day08/05split 0.497s
测试组 如果现在我们还想测试一下split
函数对中文字符的支持,我们还可以编写一个TestChinesSplit
测试函数,但是可以使用更加友好的方式添加更多的测试用例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func TestSplit3 (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 := Split(tc.input, tc.sep) if !reflect.DeepEqual(tc.want, got) { t.Errorf("excepted:%v, got:%v" , tc.want, got) } } }
执行该测试组
1 2 3 4 5 6 7 05split>go test -run="TestSplit3" -v === RUN TestSplit3 TestSplit3: split_group_test.go:28: excepted:[海自来 来自海], got:[ 海自来 来自海 ] --- FAIL: TestSplit3 (0.00s) FAIL exit status 1 FAIL github.com/DurianLollipop/StudyGo/day08/05split 0.486s
使用%#v
打印测试失败信息能更好的发现错误:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func TestSplit3 (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 := Split(tc.input, tc.sep) if !reflect.DeepEqual(tc.want, got) { t.Errorf("excepted:%#v, got:%#v" , tc.want, got) } } }
控制台输出:
1 2 3 4 5 6 7 05split>go test -run="TestSplit3" -v === RUN TestSplit3 TestSplit3: split_group_test.go:28: excepted:[]string{"海自来" , "来自海" }, got:[]string{"" , "海自来" , "来自海" , "" } --- FAIL: TestSplit3 (0.00s) FAIL exit status 1FAIL github.com/DurianLollipop/StudyGo/day08/05split 0.491s
子测试 当测试用例过多时,是没办法看出具体是哪一个测试用例失败,使用Go1.7+中新加的子测试可以解决该问题,使用t.Run()执行子测试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 func TestSplit4 (t *testing.T) { type test struct { input string sep string want []string } tests := map [string ]test{ "case_1" : {input: "a:b:c" , sep: ":" , want: []string {"a" , "b" , "c" }}, "case_2" : {input: "a:b:c" , sep: "," , want: []string {"a:b:c" }}, "case_3" : {input: "abcd" , sep: "bc" , want: []string {"a" , "d" }}, "case_4" : {input: "上海自来上来自海上" , sep: "上" , want: []string {"海自来" , "来自海" }}, } for name, tc := range tests{ t.Run(name, func (t *testing.T) { got := Split(tc.input, tc.sep) if !reflect.DeepEqual(tc.want, got) { t.Errorf("excepted:%#v, got:%#v" , tc.want, got) } }) } }
控制台输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 05split>go test -run="TestSplit4" -v === RUN TestSplit4 === RUN TestSplit4/case_1 === RUN TestSplit4/case_2 === RUN TestSplit4/case_3 === RUN TestSplit4/case_4 TestSplit4/case_4: split_sub_test.go:29: excepted:[]string{"海自来" , "来自海" }, got:[]string{"" , "海自来" , "来自海" , "" } --- FAIL: TestSplit4 (0.00s) --- PASS: TestSplit4/case_1 (0.00s) --- PASS: TestSplit4/case_2 (0.00s) --- PASS: TestSplit4/case_3 (0.00s) --- FAIL: TestSplit4/case_4 (0.00s) FAIL exit status 1FAIL github.com/DurianLollipop/StudyGo/day08/05split 0.429s
可以通过-run=RegExp
来指定运行测试用例,还可以通过/
来指定要运行的子测试,例如go test -run=TestSplit4/case_3 -v
1 2 3 4 5 6 7 05split>go test -run=TestSplit4/case_3 -v === RUN TestSplit4 === RUN TestSplit4/case_3 --- PASS: TestSplit4 (0.00s) --- PASS: TestSplit4/case_3 (0.00s) PASS ok github.com/DurianLollipop/StudyGo/day08/05split 0.643s
测试覆盖率 测试覆盖率是指你的代码被单元测试覆盖的百分比,我们通常使用的是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比率。
Go提供内置的功能来检查代码的覆盖率,使用go test -cover
来查看测试的覆盖率。
1 2 3 4 05split>go test -cover PASS coverage: 100.0% of statements ok github.com/DurianLollipop/StudyGo/day08/05split 0.535s
以上结果表明测试覆盖率达到100%。
Go语言还提供了额外的-coverprofile
参数,用来将覆盖率相关的记录信息输出到一个文件。
1 2 3 4 05split>go test -cover -coverprofile=cover.out PASS coverage: 100.0% of statements ok github.com/DurianLollipop/StudyGo/day08/05split 0.689s
执行go tool cover -html=cover.out
,使用cover
工具处理生成的记录信息,该命令会打开本地浏览器窗口生成一个HTML报告。
绿色标记的语句块表示被覆盖了,而红色的表示没有被覆盖。
基准测试 基准测试的函数格式 基准测试就是在一定的工作负载下检测程序性能的一种方法,基准测试的基本格式如下:
1 2 3 func BenchmarkNmae (b *testing.B) { }
基准测试以Benchmark
为前缀,需要一个b *testing.B
类型的参数b,基准测试必须要执行b.N
次,这样测试才有对照性,b.N
的值是系统根据实际情况去调整的,保证测试的稳定性。testing.B
拥有的方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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 ()
基准测试示例 以split包中的Split
函数编写基准测试:
1 2 3 4 5 func BenchmarkSplit (b *testing.B) { for i := 0 ; i < b.N; i++ { Split("a:b:c" , ":" ) } }
基准测试并不会默认执行,需要增加-bench
参数,通过go test -bench=Split
命令执行基准测试,输出结果如下:
1 2 3 4 5 6 7 05split>go test -bench=Split goos: windows goarch: amd64 pkg: github.com/DurianLollipop/StudyGo/day08/05split BenchmarkSplit-8 5603208 219 ns/op PASS ok github.com/DurianLollipop/StudyGo/day08/05split 2.000s
其中BenchmarkSplit-8
表示对Split函数进行基准测试,数字8
表示GOMAXPROCS
的值,这个对于并发基准测试很重要。5603208
和219 ns/op
表示每次调用Split
函数耗时219 ns
,这个结果是5603208
次调用的平均值。
使用-benchmem
参数,来获得内存分配的统计数据。
1 2 3 4 5 6 7 05split>go test -bench=Split -benchmem goos: windows goarch: amd64 pkg: github.com/DurianLollipop/StudyGo/day08/05split BenchmarkSplit-8 5334916 222 ns/op 112 B/op 3 allocs/op PASS ok github.com/DurianLollipop/StudyGo/day08/05split 2.002s
其中112 B/op
表示每次操作内存分配了112字节,3 allocs/op
表示每次操作进行了3次内存分配。
对Split
函数优化:
1 2 3 4 5 6 7 8 9 10 11 12 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 }
提前使用make函数将result初始化为一个容量够大大的切片,而不是之前的通过append函数来追加。
控制台输出:
1 2 3 4 5 6 7 05split>go test -bench=Split -benchmem goos: windows goarch: amd64 pkg: github.com/DurianLollipop/StudyGo/day08/05split BenchmarkSplit-8 11383266 105 ns/op 48 B/op 1 allocs/op PASS ok github.com/DurianLollipop/StudyGo/day08/05split 1.846s
使用make函数提前分配内存的改动,减少了2/3的内存分配次数,并减少了一般的内存分配。
性能比较函数 上面的基准测试只能得到给定操作的绝对耗时,但是在很多性能问题发生在两个不同操作之间的相对耗时,如同一个函数处理1000个元素与处理10000个甚至100万个元素的耗时差别是多少?再或者对于同一个任务究竟使用那种算法性能最佳?我们通常要对两个不同算法的实现使用相同的输入进行基准比较测试。
性能比较测试通常是一个带有参数的函数,被多个不同的Benchmark
函数传入不同的值来调用。
以测试斐波那契数列为例:
1 2 3 4 5 6 func Fib (n int ) int { if n < 2 { return n } return Fib(n-1 ) * Fib(n-2 ) }
编写性能比较函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func benchmarkFib (b *testing.B, size int ) { for i := 0 ; i < b.N; i++ { Fib(size) } } func BenchmarkFib1 (b *testing.B) { benchmarkFib(b, 1 ) } func BenchmarkFib2 (b *testing.B) { benchmarkFib(b, 2 ) } func BenchmarkFib10 (b *testing.B) { benchmarkFib(b, 10 ) } func BenchmarkFib20 (b *testing.B) { benchmarkFib(b, 10 ) }
运行基准测试:
-bench=.
代表运行该包的所有的性能基准测试
1 2 3 4 5 6 7 8 9 10 05fibonaccinumber>go test -bench=. goos: windows goarch: amd64 pkg: github.com/DurianLollipop/StudyGo/day08/05fibonaccinumber BenchmarkFib1-8 537147558 1.97 ns/op BenchmarkFib2-8 210452324 5.58 ns/op BenchmarkFib10-8 3283648 357 ns/op BenchmarkFib20-8 27069 45764 ns/op PASS ok github.com/DurianLollipop/StudyGo/day08/05fibonaccinumber 6.791s
重置时间 b.ResetTimer
之前的处理不会放到执行时间里,也不会输出到报告中,所以可以在之前做一些不计划作为测试报告的操作,例如:
1 2 3 4 5 6 7 8 func BenchmarkSplit2 (b *testing.B) { time.Sleep(5 * time.Second) b.ResetTimer() for i := 0 ; i < b.N; i++ { Split("a:b:c" , ":" ) } }
并行测试 func (b *B) RunParallel(body func(*PB))
会以并行的方式执行给定的基准测试。
RunParallel
会创建出多个goroutine
,并将b.N
分配给这些goroutine
执行,其中goroutine
数量的默认值为GOMAXPROCS
。如果用户想要增加非CPU受限(non-CPU-bound)基准测试的并行性,那么可以在RunParallel
之前调用SetPararllelism
。RunParallel
通常会与-cpu
标志一同使用。
1 2 3 4 5 6 7 8 9 func BenchmarkSplitWithParallel (b *testing.B) { b.SetParallelism(4 ) b.RunParallel(func (pb *testing.PB) { for pb.Next() { Split("a:b:c" , ":" ) } }) }
执行基准测试:
1 2 3 4 5 6 7 8 9 05split>go test -bench=. goos: windows goarch: amd64 pkg: github.com/DurianLollipop/StudyGo/day08/05split BenchmarkSplit-8 10462840 107 ns/op BenchmarkSplit2-8 10110516 107 ns/op BenchmarkSplitWithParallel-8 39056408 31.1 ns/op PASS ok github.com/DurianLollipop/StudyGo/day08/05split 29.771s
还可以通过在测试命令后面添加-cpu
参数如go test -bench=. -cpu 1
来指定使用的CPU数量。
1 2 3 4 5 6 7 8 9 05split>go test -bench=SplitWithParallel -cpu 4 goos: windows goarch: amd64 pkg: github.com/DurianLollipop/StudyGo/day08/05split BenchmarkSplitWithParallel-4 32248549 32.0 ns/op PASS ok github.com/DurianLollipop/StudyGo/day08/05split 2.049s
SetUp与TearDown 测试程序有时候需要在测试之前进行额外的设置(setUp)或在测试之后进行拆卸(tearDown)操作。
TestMain 如果测试文件包含函数func TestMain(m testint.M)
那么生成的测试会先执行TestMain(m)
,然后再运行具体测试。TestMain
运行在主goroutine
中,可以在调用m.Run
前做任何设置(setUp)和卸载(tearDown)。退出测试的时候应该使用m.Run
的返回值作为调用参数调用os.Exit
。
如何使用TestMain
来设置SetUp和TearDown:
1 2 3 4 5 6 7 8 9 10 11 func TestMain (m *testing.M) { fmt.Println("write setUp code here..." ) resCode := m.Run() fmt.Println("write teardown code here..." ) os.Exit(resCode) }
需要注意的是:在调用TestMain
时,flag.Parse
并没有被调用。所以如果TestMian
依赖于command-line标志(包括testing包的标记),则应该显示的调用flag.Parse
。
子测试的SetUp和TearDown 有时候我们可能需要为每个测试集设置SetUp与TearDown,也有可能需要为每个子测试设置SetUp与TearDown。
定义两个工具函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func setupTestCase (t *testing.T) func (t *testing.T) { t.Log("如有需要在此执行:测试之前的setup" ) return func (t *testing.T) { t.Log("如有需要在此执行:测试之后的teardown" ) } } func setupSubTest (t *testing.T) func (t *testing.T) { t.Log("如有需要在此执行:子测试之前的setup" ) return func (t *testing.T) { t.Log("如有需要在此执行:子测试之后的teardown" ) } }
使用工具函数setUp和tearDown
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 func TestSplit6 (t *testing.T) { type test struct { input string sep string want []string } tests := map [string ]test{ "case_1" : {input: "a:b:c" , sep: ":" , want: []string {"a" , "b" , "c" }}, "case_2" : {input: "a:b:c" , sep: "," , want: []string {"a:b:c" }}, "case_3" : {input: "abcd" , sep: "bc" , want: []string {"a" , "d" }}, "case_4" : {input: "上海自来上来自海上" , sep: "上" , want: []string {"" , "海自来" , "来自海" , "" }}, } teardown := setupTestCase(t) defer teardown(t) for name, tc := range tests { t.Run(name, func (t *testing.T) { teardown := setupSubTest(t) defer teardown(t) got := Split(tc.input, tc.sep) if !reflect.DeepEqual(tc.want, got) { t.Fatalf("want %#v but got %#v\n" , tc.want, got) } }) } }
测试结果输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 05split>go test -run="TestS plit6" -vwrite setUp code here... === RUN TestSplit6 TestSplit6: split_setUp_TearDown_sub_test.go:10: 如有需要在此执行:测试之前的setup === RUN TestSplit6/case_3 TestSplit6/case_3: split_setUp_TearDown_sub_test.go:18: 如有需要在此执行:子测试之前的setup TestSplit6/case_3: split_setUp_TearDown_sub_test.go:20: 如有需要在此执行:子测试之后的teardown === RUN TestSplit6/case_4 TestSplit6/case_4: split_setUp_TearDown_sub_test.go:18: 如有需要在此执行:子测试之前的setup TestSplit6/case_4: split_setUp_TearDown_sub_test.go:20: 如有需要在此执行:子测试之后的teardown === RUN TestSplit6/case_1 TestSplit6/case_1: split_setUp_TearDown_sub_test.go:18: 如有需要在此执行:子测试之前的setup TestSplit6/case_1: split_setUp_TearDown_sub_test.go:20: 如有需要在此执行:子测试之后的teardown === RUN TestSplit6/case_2 TestSplit6/case_2: split_setUp_TearDown_sub_test.go:18: 如有需要在此执行:子测试之前的setup TestSplit6/case_2: split_setUp_TearDown_sub_test.go:20: 如有需要在此执行:子测试之后的teardown TestSplit6: split_setUp_TearDown_sub_test.go:12: 如有需要在此执行:测试之后的teardown --- PASS: TestSplit6 (0.00s) --- PASS: TestSplit6/case_3 (0.00s) --- PASS: TestSplit6/case_4 (0.00s) --- PASS: TestSplit6/case_1 (0.00s) --- PASS: TestSplit6/case_2 (0.00s) PASS write teardown code here... ok github.com/DurianLollipop/StudyGo/day08/05split 0.119s
示例函数 示例函数的格式 被go test
特殊对待的第三种函数就是示例函数,他们的函数名以Example
为前缀。他们既没有参数,也没有返回值。
示例函数示例 1 2 3 4 5 func ExampleSplit () { fmt.Println(Split("a:b:c" , ":" )) }
编写示例函数又如下三个好处:
1、示例函数能够作为文档直接使用,例如基于Web的godoc中能把示例函数与对应的函数或包关联。
2、示例函数只要包含了//Output:
也是可以通过go test
运行的可执行测试。
1 2 3 4 5 05split>go test -run=ExampleSplit write setUp code here... PASS write teardown code here... ok github.com/DurianLollipop/StudyGo/day08/05split 0.222s
3、示例函数提供了可以直接运行的示例代码,可以直接在golang
的godoc
文档服务器使用Go Playground
运行示例代码。