性能测试函数以Benchmark开头,b *testing.B为参数, b.N为执行次数,次数不是固定的,是一秒内能执行的次数,不同的函数 次数不一样
split.go
package split
import ("strings")
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):] // 这里使用len(sep)获取sep的长度i = strings.Index(s, sep)}result = append(result, s)return}
split_test.go
package split_test
import ("github.com/business_group/test_project/split""testing")
func BenchmarkSplit(b *testing.B) {for i := 0; i < b.N; i++ {split.Split("a,b,c,d,e,f", ",")}}
执行性能测试:`go test -bench=Split`
其中BenchmarkSplit-4
表示对Split函数进行基准测试,数字4
表示GOMAXPROCS
的值,这个对于并发基准测试很重要。3045511
和402ns/op
表示每次调用Split
函数耗时402ns
,这个结果是3045511
次调用的平均值。
查看内存占用情况:`go test -bench=Split -benchmem`
其中,240 B/op
表示每次操作内存分配了240字节,4 allocs/op
则表示每次操作进行了4次内存分配。 我们将我们的Split
函数优化如下:
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):] // 这里使用len(sep)获取sep的长度
i = strings.Index(s, sep)
}
result = append(result, s)
return
}
这一次我们提前使用make函数将result初始化为一个容量足够大的切片,而不像之前一样append函数来追加。我们来看一下这个改进会带来多大的性能提升:
性能比较函数(重要)
有的函数,跑10次、100次没问题,但跑100万次,1000万次会有性能的显著下降
例如斐波那契数列,算1的时候很快,算到100就很卡
上面的基准测试只能得到给定操作的绝对耗时,但是在很多性能问题是发生在两个不同操作之间的相对耗时,比如同一个函数处理1000个元素的耗时与处理1万甚至100万个元素的耗时的差别是多少?再或者对于同一个任务究竟使用哪种算法性能最佳?我们通常需要对两个不同算法的实现使用相同的输入来进行基准比较测试。
性能比较函数通常是一个带有参数的函数,被多个不同的Benchmark函数传入不同的值来调用。举个例子如下:
// fib.go
// Fib 是一个计算第n个斐波那契数的函数
func Fib(n int) int {
if n < 2 {
return n
}
return Fib(n-1) + Fib(n-2)
}
性能比较函数:
// fib_test.go
//性能比较测试
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=Fib2`
使用`go test -bench=. `运行所有的。默认情况下,每个基准测试至少运行1秒。如果在Benchmark函数返回时没有到1秒,则b.N的值会按1,2,5,10,20,50,…增加,并且函数再次运行。
如果你想强制执行20s:`go test -bench=Fib40 -benchtime=20s
`
重置时间
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
会创建出多个goroutine
,并将b.N
分配给这些goroutine
执行, 其中goroutine
数量的默认值为GOMAXPROCS
。用户如果想要增加非CPU受限(non-CPU-bound)基准测试的并行性, 那么可以在RunParallel
之前调用SetParallelism
。RunParallel
通常会与-cpu
标志一同使用。
func BenchmarkSplitParallel(b *testing.B) {
b.SetParallelism(1) // 设置使用的CPU数
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Split("沙河有沙又有河", "沙")
}
})
}
执行:`go test -bench=Split`
还可以通过在测试命令后添加-cpu
参数如`go test -bench=. -cpu 1`
来指定使用的CPU数量。
Setup与TearDown
测试程序有时需要在测试之前进行额外的设置(setup)或在测试之后进行拆卸(teardown)。
通过在*_test.go
文件中定义TestMain
函数来可以在测试之前进行额外的设置(setup)或在测试之后进行拆卸(teardown)操作。
如果测试文件包含函数: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) } }) } }
测试结果:
split $ 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 github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s
示例函数
示例函数用于生成文档
被go test
特殊对待的第三种函数就是示例函数,它们的函数名以Example
为前缀。它们既没有参数也没有返回值。标准格式如下:
func ExampleName() { // ... }