[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
// # 1.测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头。
func TestAdd(t *testing.T){ ... }
func TestSum(t *testing.T){ ... }
func TestLog(t *testing.T){ ... }

// # 2.例如,我们测试一个数的绝对值是否与我们设置定值一致,如果测试不一致则输出t.Errorf()方法中的自定义错误信息。
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
// 06unit/splitstring/splitstr.go
// # 自定义实现切割字符串
package splitstring
import (
"strings"
)
// 1.Split 切割自定义实现函数
func Split(str string, sep string) []string {
var ret []string
index := strings.Index(str, sep)
seplen := len(sep)
// 2.sep 在字符串索引中大于等于0时证明有字符串
for index >= 0 {
splitstr := str[:index]
// 3.过滤分割字符前空以及后空
if splitstr != "" {
ret = append(ret, splitstr)
}
// 4.解决sep为多个字符的情况。
str = str[index+seplen:]
index = strings.Index(str, sep)
}
// 5.将最后的字符也放入ret数组中,并返回给调用者
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
// # 06unit/splitstring/split_test.go
package splitstring
import (
"reflect"
"testing"
)
// 1.注意测试的函数格式采用驼峰命名法,且首字母必须大写,其次是必须接收一个`*testing.T`类型参数.
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

// # -v 指定一个目录将该目录下_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] // 可以清除的看到 Test2Split 测试用例没有成功
--- FAIL: Test2Split (0.00s)
FAIL
FAIL weiyigeek.top/studygo/Day08/06unit/splitstring 0.002s // 全部测试函数执行运行时间
FAIL

// # -run 指定一个想要执行的单元测试函数,例如此处是 Test1Split(t *testing.T)
➜ splitstring go test -v -run=Test1Split
=== RUN Test1Split
--- PASS: Test1Split (0.00s)
PASS
ok weiyigeek.top/studygo/Day08/06unit/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/06unit/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 splitstring
import (
"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

// 下述说第四的一个单元测试有问题,此时你可以看到返回的 [唯一极客] 与我们预定义的 [唯一极客] 是不一致的!
// 此种情况下十分推荐使用%#v的格式化方式。
➜ 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/06unit/grouptest 0.002s
FAIL

// 下面我们修改测试用例错误提示的部分,此时可以看到单元测试全部通过。
➜ grouptest go test -v .
=== RUN TestGroupSplit
--- PASS: TestGroupSplit (0.00s)
PASS
ok weiyigeek.top/studygo/Day08/06unit/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
// 06unit/subtest/split_sub_test.go
package subtest
import (
"reflect"
"testing"
)
// 子测试 示例
func TestChildSplit(t *testing.T) {
// 1.同样定义一个subTestCase
type subTestCase struct {
str, sep string
want []string
}
// 2.声明定义一个Map类型的testGroup变量
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"}},
}
// 3.遍历测试组,逐一执行测试用例
for k, v := range testGroup {
println("测试名称: ", k)
// 4.然后使用t.Run()执行子测试
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
// # 可以发现我们期望的www3.weiyigeek.top与Split函数实际返回的www.weiyigeek.top是不相同的。
=== 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/06unit/subtest 0.003s
FAIL

// # 修正期望值后通过 `-run=RegExp` 来指定运行的测试用例, 还可以通过/来指定要运行的子测试用例,例如
➜ 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/06unit/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报告。

WeiyiGeek.代码覆盖率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
// weiyigeek.top/packeage/myself/fibonacci.go

package myself
func 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
// weiyigeek.top/studygo/Day08/06unit/benchmarktest/benchmark_test.go

package benchmarktest
import (
"fmt"
"testing"
custom "weiyigeek.top/packeage/myself"
)
func BenchmarkFibonacci(b *testing.B) {
// fmt.Printf("Fibonacci(%d) = %d\n", 10, custom.Fibonacci(10))
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]
# 需要增加`-bench`参数,所以我们通过执行`go test -bench=基准方法名称`命令执行基准测试
# --run=none 避免运行普通的测试函数, 因为一般不可能有函数名匹配 none
➜ 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
// weiyigeek.top/packeage/myself/splitstr.go
// 自定义实现切割字符串
package myself
import (
"strings"
)
// 1.Split 切割自定义实现函数。
func Split(str string, sep string) (result []string) {
// 2.提前使用make函数将result初始化为一个容量足够大的切片。
result = make([]string, 0, strings.Count(str, sep)+1)
index := strings.Index(str, sep)
// 3.sep 在字符串索引中大于-1时证明有字符串。
for index > -1 {
splitstr := str[:index]
// 4.过滤分割字符前空以及后空。
if splitstr != "" {
result = append(result, str[:index])
}
// 5.再次获取分割后的字符串。
str = str[index+len(sep):]
index = strings.Index(str, sep)
}
// 6.将最后的字符也放入ret数组中。
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降到了48B/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 performmancetest
import (
"testing"
custom "weiyigeek.top/packeage/myself"
)
// 注意此处调用的函数名称是小写
func benchmarkFibonacci(b *testing.B, num int) {
for i := 0; i < b.N; i++ {
custom.Fibonacci(num)
}
}
// 基准测试的函数名仍然是以Benchmark_开头
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
# 此处基准测试时间为20s
➜ 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 goroutinetest
import (
"testing"
"time"
custom "weiyigeek.top/packeage/myself"
)
func BenchmarkSplitParaller(b *testing.B) {
// 假设需要做一些耗时的无关操作
time.Sleep(5 * time.Second)

// 增加非CPU受限(non-CPU-bound)基准测试的并行性,即设置使用的CPU数
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
// 1.并行测试与时间重置测试
➜ goroutinetest go test -bench=. -v -benchmem
goos: linux
goarch: amd64
pkg: weiyigeek.top/studygo/Day08/06unit/goroutinetest
cpu: Intel(R) Core(TM) i5-3570 CPU @ 3.40GHz
BenchmarkSplitParaller
BenchmarkSplitParaller-4 34091397 31.58 ns/op 48 B/op 1 allocs/op
PASS
ok weiyigeek.top/studygo/Day08/06unit/goroutinetest 26.130s

// 2.注释 time.Sleep(5 * time.Second) 和 b.ResetTimer() 后的结果
➜ goroutinetest go test -bench=. -v
goos: linux
goarch: amd64
pkg: weiyigeek.top/studygo/Day08/06unit/goroutinetest
cpu: Intel(R) Core(TM) i5-3570 CPU @ 3.40GHz
BenchmarkSplitParaller
BenchmarkSplitParaller-4 35342922 32.61 ns/op
PASS
ok weiyigeek.top/studygo/Day08/06unit/goroutinetest 1.955s

// 3.再注释 b.SetParallelism(2) 此时利用 -cpu 参数指定两个CPU进行结果查看
➜ goroutinetest go test -bench=. -v -cpu 2 --benchmem
goos: linux
goarch: amd64
pkg: weiyigeek.top/studygo/Day08/06unit/goroutinetest
cpu: Intel(R) Core(TM) i5-3570 CPU @ 3.40GHz
BenchmarkSplitParaller
BenchmarkSplitParaller-2 21463302 54.96 ns/op 48 B/op 1 allocs/op
PASS
ok weiyigeek.top/studygo/Day08/06unit/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 setupteardown
import (
"flag"
"fmt"
"os"
"testing"
)
// M是传递给TestMain函数以运行实际测试的类型。
// 执行第一步
func TestMain(m *testing.M) {
var name string
flag.StringVar(&name, "name", "张三", "姓名")
flag.Parse() // 如果 TestMain 使用了 flags 此处应该加上解析
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】") // 测试TestUser函数定义执行
fmt.Println("正在测试执行第二步: 开始测试子测试函数")
t.Run("调用 testFunc 中", testFunc) // 调用测试TestEnd函数,注意第一个字符串参数如有空格将会被下划线替代。
}

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】") // 测试TestEnd函数定义执行
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜  setupteardown go test -v
## Step1.write setup code here...TestMain # 关键点 m.Run() 之前的代码块
=== RUN TestUser
# Step2.write setup code here...【TestUser】
正在测试执行第二步: 开始测试子测试函数
=== RUN TestUser/调用_testFunc_中
这时测试的testFunc的函数,名称为testFunc
--- PASS: TestUser (5.00s) # 可以看到延迟的5s
--- PASS: TestUser/调用_testFunc_中 (5.00s)
=== RUN TestEnd
# Step3.write setup code here...【TestEnd】
--- PASS: TestEnd (0.00s)
PASS
## Step4.write teardown code here... 0 # 关键点 m.Run() 之后的代码块
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,也有可能需要为每个子测试设置SetupTeardown

下面我们定义两个函数工具函数以及单元组测试代码如下:

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 childsetupteardown
import (
"reflect"
"testing"
custom "weiyigeek.top/packeage/myself"
)

// 1.测试集的Setup与Teardown
func setupTestCase(t *testing.T) func(t *testing.T) {
t.Log("[测试集] 之前的 setup.....")
return func(t *testing.T) {
t.Log("[测试集] 之后的 teardown.....")
}
}

// 2.子测试的Setup与Teardown
func setupSubTest(t *testing.T) func(t *testing.T) {
t.Log("#[子测试集] 之前的 setup-------")
return func(t *testing.T) {
t.Log("#[子测试集] 之后的 teardown--------")
}
}

// 3.单元测试函数
func TestSplit(t *testing.T) {
type test struct { // 3.1 定义test结构体
input string
sep string
want []string
}
tests := map[string]test{ // 3.2 测试用例使用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: "博客.blog.weiyigeek.top", sep: ".", want: []string{"博客", "blog", "weiyigeek", "top"}},
}

teardownTestCase := setupTestCase(t) // 3.3 测试之前执行setup操作 【关键点】
defer teardownTestCase(t) // 3.4 测试之后执行testdoen操作 【关键点】

// 3.5 循环遍历子测试
for name, tc := range tests {
t.Run(name, func(t *testing.T) { // 3.6 使用t.Run()执行子测试
teardownSubTest := setupSubTest(t) // 3.7 子测试之前执行setup操作 【关键点】
defer teardownSubTest(t) // 3.8 测试之后执行testdoen操作【关键点】
got := custom.Split(tc.input, tc.sep) // 3.9 字符串分割返回结果
if !reflect.DeepEqual(got, tc.want) { // 3.10 利用反射函数判断两个数组
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: #[子测试集] 之前的 setup-------
st_child_test.go:22: #[子测试集] 之后的 teardown--------
=== RUN TestSplit/wrong_sep
st_child_test.go:20: #[子测试集] 之前的 setup-------
st_child_test.go:22: #[子测试集] 之后的 teardown--------
=== RUN TestSplit/more_sep
st_child_test.go:20: #[子测试集] 之前的 setup-------
st_child_test.go:22: #[子测试集] 之后的 teardown--------
=== RUN TestSplit/leading_sep
st_child_test.go:20: #[子测试集] 之前的 setup-------
st_child_test.go:22: #[子测试集] 之后的 teardown--------
=== 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
# 设置 golang.org 代理
export GOPROXY=https://goproxy.io
export GO111MODULE=on
# 此命令会访问官网下载godoc以及相关依赖包
➜ 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
# 项目构建生成godoc
➜ go build golang.org/x/tools/cmd/godoc
# 验证环境安装 (此时他会将你go.mod项目的下所有除_test.go文件的有备注的包进行显示)
# 运行 godoc 将会自动生成API文档
# http://localhost:6060/pkg/
➜ /home/weiyigeek/app/program/project/go/bin/godoc

WeiyiGeek.godoc显示项目文档

WeiyiGeek.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
// 文件必须放在 example_test 包目录下, 名字必须为 example_xxx_test.go

// Package example_test 为 example 包的示例
package exampletest

// 此注释将会被展示在页面上
// 此函数将被展示在OverView区域
func ExampleName() {
fmt.Println("Hello OverView")
// Output:
// Hello OverView
}
func ExampleName_test() {
fmt.Println("Hello Test")
// Output:
// Hello Test
}

Tips : 通常情况下包名_test.goexample_test.go或者example_包名_test.go都在同一个包下。


为你的代码编写示例代码有如下三个用处:

  • 1.示例函数能够作为文档直接使用,例如基于web的godoc中能把示例函数与对应的函数或包相关联。
  • 2.示例函数只要包含了//Output也是可以通过go test运行的可执行测试,例如:// Output: <换行符>// Hello OverView
  • 3.示例函数提供了可以直接运行的示例代码,可以直接在golang.orggodoc文档服务器上使用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 exampletest 包文档生成示例
//----- example_test.go -----
package exampletest

import (
"fmt"
custom "weiyigeek.top/packeage/myself"
)

// 此注释将会被展示在页面上
// 此函数将被展示在OverView区域
func Example() {
fmt.Println("Hello OverView")
// Output:
// Hello OverView
}

// 此函数将被展示在OverView区域, 并展示noOutput标签
func Example_noOutput() {
fmt.Println("Hello OverView")
// (Output: ) 非必须, 存在时将会展示输出结果, 此处不存在则不会在go test -v 结果集中输出
}

// 此函数将被展示在Function区域
// ExampleSplit 字符串分割函数使用说明
func Example_funSplit() {
res1 := custom.Split("www.weiyigeek.top", ".")
res2 := custom.Split("blog.weiyigeek.top", ".")
fmt.Println(res1)
fmt.Println(res2)
// Output:
// [www weiyigeek top]
// [blog weiyigeek top]
}

// 此函数将被展示在Function区域
// ExampleFibonacci 斐波那契数列生成说明
func Example_funFibonacci() {
fib := custom.Fibonacci(3)
fmt.Println(fib)
// Output:
// 2
}

执行结果

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