[TOC]
0x00 Go语言基础之Unit(单元)测试 描述: 日常开发中, 测试是不能缺少的. 通常国内的程序员都不太关注单元测试这一部分, 俗话说不写测试的开发不是好程序猿,我认为每一位开发者都应该了解 TDD(Test Driven Development-测试驱动开发)
,所以本章将主要介绍下在Go语言中如何做单元测试
和基准测试
。
Tips: 编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具(再次体现Go语言的优秀
)。
不过在介绍之前,我们先介绍一个Go语言的标准库为我们提供的单元测试与基准测试的辅助工具,有一个叫做 testing 的测试框架, 可以用于单元测试和性能测试,它是和go test
命令一起使用的,它是一个按照一定约定和组织的测试代码的驱动程序。
非常注意、非常注意在包目录内,所有以_test.go
为后缀名的源代码文件都是go test
测试的一部分,不会被go build
编译到最终的可执行文件中。
在*_test.go
文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。
类型
格式
作用
测试函数
函数名前缀为Test
测试程序的一些逻辑行为是否正确
基准函数
函数名前缀为Benchmark
测试函数的性能
示例函数
函数名前缀为Example
为文档提供示例文档
Tips : go test
命令会遍历所有的*_test.go
文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。
1.单元测试 描述: 类似于细胞是构成我们身体的基本单位,一个软件程序也是由很多单元组件构成的,单元组件可以是函数、结构体、方法和最终用户可能依赖的任意东西,总之我们需要确保这些组件是能够正常运行的。
即: 单元测试是一些利用各种方法测试单元组件的程序
,它会将结果与预期输出进行比较
。
单元测试有得又叫测试函数,每个测试函数必须导入testing
包,其语法格式如下所示: func TestName(t *testing.T){Code Test()}
;
其中 参数t
用于报告测试失败和附加的日志信息 , testing.T
的拥有的方法如下:
[TOC]
0x00 Go语言基础之Unit(单元)测试 描述: 日常开发中, 测试是不能缺少的. 通常国内的程序员都不太关注单元测试这一部分, 俗话说不写测试的开发不是好程序猿,我认为每一位开发者都应该了解 TDD(Test Driven Development-测试驱动开发)
,所以本章将主要介绍下在Go语言中如何做单元测试
和基准测试
。
Tips: 编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具(再次体现Go语言的优秀
)。
不过在介绍之前,我们先介绍一个Go语言的标准库为我们提供的单元测试与基准测试的辅助工具,有一个叫做 testing 的测试框架, 可以用于单元测试和性能测试,它是和go test
命令一起使用的,它是一个按照一定约定和组织的测试代码的驱动程序。
非常注意、非常注意在包目录内,所有以_test.go
为后缀名的源代码文件都是go test
测试的一部分,不会被go build
编译到最终的可执行文件中。
在*_test.go
文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。
类型
格式
作用
测试函数
函数名前缀为Test
测试程序的一些逻辑行为是否正确
基准函数
函数名前缀为Benchmark
测试函数的性能
示例函数
函数名前缀为Example
为文档提供示例文档
Tips : go test
命令会遍历所有的*_test.go
文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。
1.单元测试 描述: 类似于细胞是构成我们身体的基本单位,一个软件程序也是由很多单元组件构成的,单元组件可以是函数、结构体、方法和最终用户可能依赖的任意东西,总之我们需要确保这些组件是能够正常运行的。
即: 单元测试是一些利用各种方法测试单元组件的程序
,它会将结果与预期输出进行比较
。
单元测试有得又叫测试函数,每个测试函数必须导入testing
包,其语法格式如下所示: func TestName(t *testing.T){Code Test()}
;
其中 参数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
基础示例: 1 2 3 4 5 6 7 8 9 10 11 12 func TestAdd (t *testing.T) { ... }func TestSum (t *testing.T) { ... }func TestLog (t *testing.T) { ... }func TestAbs (t *testing.T) { got := Abs(-1 ) if got != 1 { t.Errorf("Abs(-1) = %d; want 1" , got) } }
1.1 测试函数 说了前面说了这么多我们不如实践一把。
示例1.简单的测试函数示例 首先,我们定义一个split的包,包中定义了一个Split函数,具体实现如下: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 package splitstringimport ( "strings" ) func Split (str string , sep string ) []string { var ret []string index := strings.Index(str, sep) seplen := len (sep) for index >= 0 { splitstr := str[:index] if splitstr != "" { ret = append (ret, splitstr) } str = str[index+seplen:] index = strings.Index(str, sep) } ret = append (ret, str) return ret }
其次,在06unit/splitstring
目录下创建一个split_test.go
的文件,它实现了我们的单元测试。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 package splitstringimport ( "reflect" "testing" ) func Test1Split (t *testing.T) { ret := Split("abcadeafg" , "a" ) want := []string {"bc" , "de" , "fg" } if !reflect.DeepEqual(ret, want) { t.Errorf("Want: %v But Got:%v \n" , want, ret) } } func Test2Split (t *testing.T) { ret := Split("abcadeafg" , "ad" ) want := []string {"abc" , "eag" } if !reflect.DeepEqual(ret, want) { t.Errorf("Want: %v But Got:%v \n" , want, ret) } }
执行结果:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ➜ splitstring ls splitstr.go split_test.go ➜ splitstring go test -v . === RUN Test1Split --- PASS: Test1Split (0.00s ) === RUN Test2Split split_test.go :24 : Want: [abc eag] But Got:[abc eafg] --- FAIL: Test2Split (0.00s ) FAIL FAIL weiyigeek.top/studygo/Day08/06 unit/splitstring 0.002s FAIL ➜ splitstring go test -v -run=Test1Split === RUN Test1Split --- PASS: Test1Split (0.00s ) PASS ok weiyigeek.top/studygo/Day08/06 unit/splitstring 0.003s
Tips: 非常注意,当我们修改了我们的代码之后不要仅仅执行那些失败的测试函数,我们应该完整的运行所有的测试,保证不会因为修改代码而引入了新的问题。1 2 3 4 5 6 7 ➜ splitstring go test -v . === RUN Test1Split --- PASS: Test1Split (0.00s ) === RUN Test2Split --- PASS: Test2Split (0.00s ) PASS ok weiyigeek.top/studygo/Day08/06 unit/splitstring 0.003s
1.2 测试组 描述: 此时,假如我们还想测试一下split函数
对中文字符串的支持,此时我们可以再编写一个TestChineseSplit测试函数,但是我们也可以使用如下更友好的一种方式来添加更多的测试用例。那就是使用测试组。
测试组示例: 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 package splitstringimport ( "reflect" "testing" ) func TestGroupSplit (t *testing.T) { type testCase struct { str string sep string want []string } testGroup := []testCase{ testCase{"abceafgh" , "a" , []string {"bce" , "fgh" }}, testCase{"a:b:c" , ":" , []string {"a" , "b" , "c" }}, {str: "abcdef" , sep: "cd" , want: []string {"ab" , "ef" }}, {str: "WeiyiGeek切割唯一极客" , sep: "切割" , want: []string {"WeiyiGeek" , "唯一极客!" }}, } for index, tc := range testGroup { got := Split(tc.str, tc.sep) if !reflect.DeepEqual(got, tc.want) { t.Fatalf("index %v,Want=%v not equal got=%v \n" , index+1 , tc.want, got) } } }
执行结果:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ➜ grouptest ls split_group_test.go splitstr.go ➜ grouptest go test -v . === RUN TestGroupSplit split_group_test.go :31 : index 4 ,Want=[WeiyiGeek 唯一极客!] not equal got=[WeiyiGeek 唯一极客] --- FAIL: TestGroupSplit (0.00s ) FAIL FAIL weiyigeek.top/studygo/Day08/06 unit/grouptest 0.002s FAIL ➜ grouptest go test -v . === RUN TestGroupSplit --- PASS: TestGroupSplit (0.00s ) PASS ok weiyigeek.top/studygo/Day08/06 unit/grouptest 0.002s
1.3 子测试 描述: 当测试用例较多时,我们采用上面的方式不能一眼看出具体是那些测试用例失败了,此时我们可以为每个测试案例加上名称, 当然更好的方式还是今天的主人公子测试
。
子测试 是在Go 1.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 27 28 29 30 31 32 33 package subtestimport ( "reflect" "testing" ) func TestChildSplit (t *testing.T) { type subTestCase struct { str, sep string want []string } testGroup := map [string ]subTestCase{ "Subtest_1" : {"abceafgh" , "a" , []string {"bce" , "fgh" }}, "Subtest_2" : {"a:b:c" , ":" , []string {"a" , "b" , "c" }}, "Subtest_3" : {"abcdef" , "cd" , []string {"ab" , "ef" }}, "Subtest_4" : {"WeiyiGeek切割唯一极客" , "切割" , []string {"WeiyiGeek" , "唯一极客" }}, "Subtest_5" : {"http://www.weiyigeek.top" , "//" , []string {"http:" , "www.weiyigeek.top" }}, } for k, v := range testGroup { println ("测试名称: " , k) t.Run(k, func (t *testing.T) { got := Split(v.str, v.sep) if !reflect.DeepEqual(got, v.want) { t.Fatalf("index %v,Want=%v not equal got=%v \n" , k, v.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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 ➜ subtest ls splitstr.go split_sub_test.go ➜ subtest go test -v . === RUN TestChildSplit 测试名称: Subtest_1 === RUN TestChildSplit/Subtest_1 测试名称: Subtest_2 === RUN TestChildSplit/Subtest_2 测试名称: Subtest_3 === RUN TestChildSplit/Subtest_3 测试名称: Subtest_4 === RUN TestChildSplit/Subtest_4 测试名称: Subtest_5 === RUN TestChildSplit/Subtest_5 split_sub_test.go :32 : index Subtest_5,Want=[http: www3.weiyigeek.top] not equal got=[http: www.weiyigeek.top] --- FAIL: TestChildSplit (0.00s ) --- PASS: TestChildSplit/Subtest_1 (0.00s ) --- PASS: TestChildSplit/Subtest_2 (0.00s ) --- PASS: TestChildSplit/Subtest_3 (0.00s ) --- PASS: TestChildSplit/Subtest_4 (0.00s ) --- FAIL: TestChildSplit/Subtest_5 (0.00s ) FAIL FAIL weiyigeek.top/studygo/Day08/06 unit/subtest 0.003s FAIL ➜ subtest go test -v -run=TestChildSplit/Subtest_5 === RUN TestChildSplit 测试名称: Subtest_1 测试名称: Subtest_2 测试名称: Subtest_3 测试名称: Subtest_4 测试名称: Subtest_5 === RUN TestChildSplit/Subtest_5 --- PASS: TestChildSplit (0.00s ) --- PASS: TestChildSplit/Subtest_5 (0.00s ) PASS ok weiyigeek.top/studygo/Day08/06 unit/subtest 0.002s
1.4 测试覆盖率 描述: Go语言还为开发者们提供内置功能来检查你的代码覆盖率(代码被测试套件覆盖的百分比
), 通过使用go test -cover
来查看测试覆盖率以及go tool conver
来生成HTML格式表示测试覆盖率。
Tips: 通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。
例如,此时我们使用1.1
单元测试中的示例进行。1 2 3 4 ➜ splitstring go test -cover PASS coverage: 100.0% of statements ok weiyigeek.top/studygo/Day08/06unit/splitstring 0.002s
此外,Go还提供了一个额外的-coverprofile
参数,用来将覆盖率相关的记录信息输出到一个文件。例如:1 2 3 4 5 6 7 8 ➜ splitstring go test -cover -coverprofile=cover.out -v . === RUN Test1Split --- PASS: Test1Split (0.00s) === RUN Test2Split --- PASS: Test2Split (0.00s) PASS coverage: 100.0% of statements ok weiyigeek.top/studygo/Day08/06unit/splitstring 0.002s coverage: 100.0% of statements
上面的命令会将覆盖率相关的信息输出到当前文件夹下面的cover.out
文件中
最后,我们执行go tool cover -html=cover.out
,使用cover工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成一个HTML报告。
图中每个用绿色标记
的语句块表示被覆盖
了,而红色
的表示没有被覆盖
。
2.基准测试 Q: 什么是基准测试?
答: 在一定的工作负载之下检测程序性能的一种方法.
基准测试的基本语法格式如下: func BenchmarkName(b *testing.B){ code test... }
基准测试以Benchmark
为前缀,需要一个*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()
简单示例:
1 2 3 4 5 func BenchmarkHello(b *testing.B) { for i := 0; i < b.N; i++ { fmt.Sprintf("hello") } }
Tips: 默认情况下,每个基准测试至少运行1秒。如果在Benchmark函数返回时没有到1秒,则b.N的值会按 1,2,5,10,20,50,…
增加,并且函数再次运行。
2.1 基准测试用例 描述: 此处,我们利用斐波那契函数来进行基准测试。
斐波那契函数:
1 2 3 4 5 6 7 8 9 10 11 12 package myselffunc Fibonacci (number int ) int { if number == 0 { return 0 } if number == 1 || number == 2 { return 1 } return Fibonacci(number-1 ) + Fibonacci(number-2 ) }
然后我们在benchmarktest
包中编写基准测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package benchmarktestimport ( "fmt" "testing" custom "weiyigeek.top/packeage/myself" ) func BenchmarkFibonacci (b *testing.B) { for i := 0 ; i < b.N; i++ { custom.Fibonacci(10 ) } }
执行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ➜ benchmarktest go test . ok weiyigeek.top/studygo/Day08/06unit/benchmarktest 0.002s [no tests to run] ➜ 06unit go test -v -bench=BenchmarkFibonacci --run=none ./benchmarktest goos: linux goarch: amd64 pkg: weiyigeek.top/studygo/Day08/06unit/benchmarktest cpu: Intel(R) Core(TM) i5-3570 CPU @ 3.40GHz BenchmarkFibonacci BenchmarkFibonacci-4 4495600 257.8 ns/op PASS ok weiyigeek.top/studygo/Day08/06unit/benchmarktest 1.434s
由上面的结果可知, BenchmarkFibonacci-4
表示对Fibonacci函数进行基准测试,而数字4表示GOMAXPROCS
的值,这个对于并发基准测试很重要。
然后是4495600和257.8 ns/op
表示一共调用了4495600
次且每次平均调用Fibonacci函数耗时257.8ns(纳秒)
补充说明,我们还可以为基准测试添加-benchmem
参数,来获得内存分配的统计数据,此时为了更好的观察,我们将Fibonacci()函数换做前面的Split()函数进行基准测试分析。
1 2 3 4 5 6 7 8 9 10 11 12 package benchmarktest import ( "testing" custom "weiyigeek.top/packeage/myself" ) func BenchmarkSplit(b *testing.B) { for i := 0; i < b.N; i++ { custom.Split("http://www.weiyigeek.top", ".") } }
执行结果:
1 2 3 4 5 6 7 8 9 $ 06unit go test -v -bench=BenchmarkSplit -benchmem --run=none ./benchmarktest goos: linux goarch: amd64 pkg: weiyigeek.top/studygo/Day08/06unit/benchmarktest cpu: Intel(R) Core(TM) i5-3570 CPU @ 3.40GHz BenchmarkSplit BenchmarkSplit-4 4342447 259.3 ns/op 112 B/op 3 allocs/op PASS ok weiyigeek.top/studygo/Day08/06unit/benchmarktest 1.413s
其中, 112 B/op
表示每次操作内存分配了112字节,3 allocs/op
则表示每次操作进行了3次内存分配, 其次是执行了4342447次,平均每次耗费259.3ns。
上面发生了三次内存分配,我还可以优化我们的Split()函数,此处我们使用make函数将result初始化为一个容量足够大的切片,而不再像之前一样通过调用append函数来追加。
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 package myselfimport ( "strings" ) func Split (str string , sep string ) (result []string ) { result = make ([]string , 0 , strings.Count(str, sep)+1 ) index := strings.Index(str, sep) for index > -1 { splitstr := str[:index] if splitstr != "" { result = append (result, str[:index]) } str = str[index+len (sep):] index = strings.Index(str, sep) } result = append (result, str) return }
优化完毕后,我们再次执行基准测试命令,查看上面改动后会带来多大的性能提升。
1 2 3 4 5 6 7 8 9 $ 06unit go test -v -bench=BenchmarkSplit -benchmem --run=none ./benchmarktest goos: linux goarch: amd64 pkg: weiyigeek.top/studygo/Day08/06unit/benchmarktest cpu: Intel(R) Core(TM) i5-3570 CPU @ 3.40GHz BenchmarkSplit BenchmarkSplit-4 8726422 121.1 ns/op 48 B/op 1 allocs/op PASS ok weiyigeek.top/studygo/Day08/06unit/benchmarktest 1.201s
可以看到上面这个优化, 可以看到 allocs 内存分配次数降到了1,并且每次操作内存分配的字节数也从112
降到了48
B/op, 基准测试执行的总次数在增加而平均每次执行的时间在减少,可以看到就是优化这么一个小小的点就可以带来性能的提升,所以在一些大程序中基准测试则显示的尤为重要。
2.2 性能比较用例 描述: 上面的基准测试只能得到给定操作的绝对耗时,但是在很多性能问题是发生在两个不同操作之间的相对耗时,比如同一个函数处理1000个元素的耗时与处理1万甚至100万个元素的耗时的差别是多少?再或者对于同一个任务究竟使用哪种算法性能最佳?
我们通常需要对两个不同算法的实现使用相同的输入来进行基准比较测试。
性能比较函数
通常是一个带有参数的函数,被多个不同的Benchmark函数传入不同的值来调用, 其语法格式如下
1 2 3 4 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 ) }
此处我们还是采用上面编写的斐波那契函数
,进行在计算不同值的情况下的性能比较函数,此处我们修改编写一下基准测试比较函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package performmancetestimport ( "testing" custom "weiyigeek.top/packeage/myself" ) func benchmarkFibonacci (b *testing.B, num int ) { for i := 0 ; i < b.N; i++ { custom.Fibonacci(num) } } func BenchmarkFib1 (b *testing.B) { benchmarkFibonacci(b, 1 ) }func BenchmarkFib2 (b *testing.B) { benchmarkFibonacci(b, 2 ) }func BenchmarkFib3 (b *testing.B) { benchmarkFibonacci(b, 3 ) }func BenchmarkFib10 (b *testing.B) { benchmarkFibonacci(b, 10 ) }func BenchmarkFib20 (b *testing.B) { benchmarkFibonacci(b, 20 ) }func BenchmarkFib40 (b *testing.B) { benchmarkFibonacci(b, 40 ) }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ➜ performancetest go test -v -bench=. -benchmem --run=none t goos: linux goarch: amd64 pkg: weiyigeek.top/studygo/Day08/06unit/performancetest cpu: Intel(R) Core(TM) i5-3570 CPU @ 3.40GHz BenchmarkFib1 BenchmarkFib1-4 486133974 2.413 ns/op 0 B/op 0 allocs/op BenchmarkFib2 BenchmarkFib2-4 342832730 3.470 ns/op 0 B/op 0 allocs/op BenchmarkFib3 BenchmarkFib3-4 159815354 7.474 ns/op 0 B/op 0 allocs/op BenchmarkFib10 BenchmarkFib10-4 4603944 253.9 ns/op 0 B/op 0 allocs/op BenchmarkFib20 BenchmarkFib20-4 37526 31440 ns/op 0 B/op 0 allocs/op BenchmarkFib40 BenchmarkFib40-4 3 477559446 ns/op 0 B/op 0 allocs/op PASS ok weiyigeek.top/studygo/Day08/06unit/performancetest 10.741s
从上面的结果可以看出,斐波那契值越小其执行次数越多,平均执行时间就越小,而随着测试数据的增大,平均执行时间变得越来越大,于此同时总执行次数也变少了。
当然我们可以指定基准测试函数,并且可以使用-benchtime
标志增加最小基准时间,以产生更准确的结果,例如:
1 2 3 4 5 6 7 8 9 10 ➜ performancetest go test -v -bench=BenchmarkFib40 -benchmem --run=none -benchtime=20s goos: linux goarch: amd64 pkg: weiyigeek.top/studygo/Day08/06unit/performancetest cpu: Intel(R) Core(TM) i5-3570 CPU @ 3.40GHz BenchmarkFib40 BenchmarkFib40-4 48 474603992 ns/op 0 B/op 0 allocs/op PASS ok weiyigeek.top/studygo/Day08/06unit/performancetest 23.277s
补充说明: 使用性能比较函数做测试的时候一个容易犯的错误就是把b.N
作为输入的大小,例如以下两个例子都是错误的示范
1 2 3 4 5 6 7 8 9 10 11 // 错误示范1.会一致执行下去,除非有退出条件,但是通常情况下不会这样去做。 func BenchmarkFibWrong(b *testing.B) { for n := 0; n < b.N; n++ { Fib(n) } } // 错误示范2 func BenchmarkFibWrong2(b *testing.B) { Fib(b.N) }
2.3 并行测试用例 描述: 有时可能你需要测试一个任务在并行时执行的性能结果,而 func (b *B) RunParallel(body func(*PB))
会以并行的方式执行给定的基准测试。
RunParallel
会创建出多个goroutine
,并将b.N
分配给这些goroutine
执行, 其中goroutine
数量的默认值为GOMAXPROCS
。
如果想要增加非CPU受限(non-CPU-bound)基准测试的并行性, 那么可以在RunParallel
之前调用b.SetParallelism()
。另外一种方式 RunParallel
通常会与-cpu
标志一同使用来指定使用的CPU数据。
如果你想在正式测试函数性能前,除去配置预加载所占耗时,则我们可以采用b.ResetTimer()
来重置计数器,它会忽略在它之前代码块执行的时间,并且也不会输出到报告之中。
基础示例: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package goroutinetestimport ( "testing" "time" custom "weiyigeek.top/packeage/myself" ) func BenchmarkSplitParaller (b *testing.B) { time.Sleep(5 * time.Second) b.SetParallelism(2 ) b.ResetTimer() b.RunParallel(func (pb *testing.PB) { for pb.Next() { custom.Split("http://blog.weiyigeek.top" , "." ) } }) }
执行结果: 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 ➜ goroutinetest go test -bench=. -v -benchmem goos: linux goarch: amd64 pkg: weiyigeek.top/studygo/Day08/06 unit/goroutinetest cpu: Intel(R) Core(TM) i5-3570 CPU @ 3.40 GHz BenchmarkSplitParaller BenchmarkSplitParaller-4 34091397 31.58 ns/op 48 B/op 1 allocs/op PASS ok weiyigeek.top/studygo/Day08/06 unit/goroutinetest 26.130s ➜ goroutinetest go test -bench=. -v goos: linux goarch: amd64 pkg: weiyigeek.top/studygo/Day08/06 unit/goroutinetest cpu: Intel(R) Core(TM) i5-3570 CPU @ 3.40 GHz BenchmarkSplitParaller BenchmarkSplitParaller-4 35342922 32.61 ns/op PASS ok weiyigeek.top/studygo/Day08/06 unit/goroutinetest 1.955s ➜ goroutinetest go test -bench=. -v -cpu 2 --benchmem goos: linux goarch: amd64 pkg: weiyigeek.top/studygo/Day08/06 unit/goroutinetest cpu: Intel(R) Core(TM) i5-3570 CPU @ 3.40 GHz BenchmarkSplitParaller BenchmarkSplitParaller-2 21463302 54.96 ns/op 48 B/op 1 allocs/op PASS ok weiyigeek.top/studygo/Day08/06 unit/goroutinetest 2.253s
由上面的结果分析可知利用b.SetParallelism(2)
比采用-cpu 2
参数指定CPU的数量效率更高,每秒可以执行的次数35342922
明显大于-cpu参数执行的结果(21463302)
,并且重置时间 b.ResetTimer()
效果还是比较明显的。
3.设置拆卸测试 描述: 有时测试程序需要在测试之前进行额外的设置(setup)
或在测试之后进行拆卸(teardown)
。
3.1 TestMain 用例 例如: 通过在*_test.go
文件中定义TestMain
函数来可以在测试之前进行额外的设置(setup)或在测试之后进行拆卸(teardown)操作。
如果测试文件包含函数: func TestMain(m *testing.M)
那么生成的测试会先调用 TestMain(m),其运行在主goroutine中, 可以在调用 m.Run 前后做任何设置(setup)和拆卸(teardown),【非常注意】退出测试的时候应该使用 m.Run 的返回值作为参数调用 os.Exit
。
使用TestMain来设置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 35 package setupteardownimport ( "flag" "fmt" "os" "testing" ) func TestMain (m *testing.M) { var name string flag.StringVar(&name, "name" , "张三" , "姓名" ) flag.Parse() fmt.Println("## Step1.write setup code here...TestMain" ) ret := m.Run() fmt.Println("## Step4.write teardown code here..." , ret) os.Exit(ret) } func TestUser (t *testing.T) { fmt.Println("# Step2.write setup code here...【TestUser】" ) fmt.Println("正在测试执行第二步: 开始测试子测试函数" ) t.Run("调用 testFunc 中" , testFunc) } func testFunc (t *testing.T) { fmt.Println("这时测试的testFunc的函数,名称为testFunc" ) time.Sleep(5 * time.Second) } func TestEnd (t *testing.T) { fmt.Println("# Step3.write setup code here...【TestEnd】" ) }
执行结果: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ➜ setupteardown go test -v === RUN TestUser 正在测试执行第二步: 开始测试子测试函数 === RUN TestUser/调用_testFunc_中 这时测试的testFunc的函数,名称为testFunc --- PASS: TestUser (5.00s) --- PASS: TestUser/调用_testFunc_中 (5.00s) === RUN TestEnd --- PASS: TestEnd (0.00s) PASS ok weiyigeek.top/studygo/Day08/06unit/setupteardown 5.007s
Tips : 注意的是在调用 TestMain 时, flag.Parse并没有被调用。所以如果TestMain 依赖于command-line标志 (包括 testing 包的标记), 则应该显示的调用flag.Parse。
Tips : 测试包中的 testing.T 与 testing.M 之间区别是前者是普通测试包,而可以在测试函数执行之前做一些其他操作。
3.2 子测试集设置拆卸 描述: 有时候我们可能需要为每个测试集设置Setup与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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package childsetupteardownimport ( "reflect" "testing" custom "weiyigeek.top/packeage/myself" ) 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--------" ) } } func TestSplit (t *testing.T) { type test struct { input string sep string want []string } 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: "博客.blog.weiyigeek.top" , sep: "." , want: []string {"博客" , "blog" , "weiyigeek" , "top" }}, } teardownTestCase := setupTestCase(t) defer teardownTestCase(t) for name, tc := range tests { t.Run(name, func (t *testing.T) { teardownSubTest := setupSubTest(t) defer teardownSubTest(t) got := custom.Split(tc.input, tc.sep) if !reflect.DeepEqual(got, tc.want) { t.Errorf("expected:%#v, got:%#v" , 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 ➜ childsetupteardown go test -v === RUN TestSplit st_child_test.go:12: [测试集] 之前的 setup..... === RUN TestSplit/simple st_child_test.go:20: st_child_test.go:22: === RUN TestSplit/wrong_sep st_child_test.go:20: st_child_test.go:22: === RUN TestSplit/more_sep st_child_test.go:20: st_child_test.go:22: === RUN TestSplit/leading_sep st_child_test.go:20: st_child_test.go:22: === CONT TestSplit st_child_test.go:14: [测试集] 之后的 teardown..... --- PASS: TestSplit (0.00s) --- PASS: TestSplit/simple (0.00s) --- PASS: TestSplit/wrong_sep (0.00s) --- PASS: TestSplit/more_sep (0.00s) --- PASS: TestSplit/leading_sep (0.00s) PASS ok weiyigeek.top/studygo/Day08/06unit/childsetupteardown 0.007s
从上面的结果可以看出 Setup 与 Teardown 在单元测试中的描述, 我们可以利用其特性预加载数据,并采用上面的b.ResetTimer()
来重置性能耗时。
4.示例生成函数 4.1 基础说明 描述: go test
特殊对待的第三种函数就是示例函数,它们的函数名以Example
为前缀,注意 它们既没有参数也没有返回值。
。
环境准备: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ➜ export GOPROXY=https://goproxy.io ➜ export GO111MODULE=on ➜ go get golang.org/x/tools/cmd/godoc go: downloading golang.org/x/tools v0.1.7 go: downloading github.com/yuin/goldmark v1.4.0 go: downloading golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d go get: added golang.org/x/tools v0.1.7 ➜ go cd $GOPATH /pkg/mod/golang.org/x/ ➜ x pwd /home/weiyigeek/app/program/project/go/pkg/mod/golang.org/x ➜ go build golang.org/x/tools/cmd/godoc ➜ /home/weiyigeek/app/program/project/go/bin/godoc
weiyigeek.top-godoc显示项目文档
语法说明:
文件必须放在当前包下
文件名以 example
开头, _
连接, test
结尾, 如: example_xxx_test.go
包名是建议是 当前包名
+ _test
, 如: strings_test
函数名称的格式 func Example[FuncName][_tag]()
函数注释会展示在页面上
函数结尾加上 // Output:
注释, 说明函数返回的值
Example示例其语法标准格式如下:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package exampletestfunc ExampleName () { fmt.Println("Hello OverView" ) } func ExampleName_test () { fmt.Println("Hello Test" ) }
Tips : 通常情况下包名_test.go
与example_test.go
或者example_包名_test.go
都在同一个包下。
为你的代码编写示例代码有如下三个用处:
1.示例函数能够作为文档直接使用,例如基于web的godoc中能把示例函数与对应的函数或包相关联。
2.示例函数只要包含了//Output
也是可以通过go test运行的可执行测试,例如:// Output: <换行符>// Hello OverView
3.示例函数提供了可以直接运行的示例代码,可以直接在golang.org
的godoc文档服务器上
使用Go Playground
运行示例代码。
4.2 示例演示 例如: 下面我们分别在上面编写的 Split()
与 Fibinacci()
函数为例,生成其使用帮助文档。
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 35 36 37 38 39 40 41 42 43 package exampletestimport ( "fmt" custom "weiyigeek.top/packeage/myself" ) func Example () { fmt.Println("Hello OverView" ) } func Example_noOutput () { fmt.Println("Hello OverView" ) } func Example_funSplit () { res1 := custom.Split("www.weiyigeek.top" , "." ) res2 := custom.Split("blog.weiyigeek.top" , "." ) fmt.Println(res1) fmt.Println(res2) } func Example_funFibonacci () { fib := custom.Fibonacci(3 ) fmt.Println(fib) }
执行结果1 2 3 4 5 6 7 8 9 ➜ example_test go test -v === RUN Example --- PASS: Example (0.00s) === RUN Example_funSplit --- PASS: Example_funSplit (0.00s) === RUN Example_funFibonacci --- PASS: Example_funFibonacci (0.00s) PASS ok weiyigeek.top/studygo/Day08/06unit/example_test 0.003s