抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >
  • 单元测试能够最小范围测试程序代码,从而保证程序的正确性和健壮性

  • 平时我们可以编写单元测试代码快速了解当前的程序模块的情况,作出问题快速定位

  • 那么,在Go该如何编写单元测试呢?那必须是go test工具

1.什么是go test?

Go语言中自带有一个轻量级的测试框架testing,可以使用go test命令来实现单元测试和性能基准测试!

利用go test无须进行任何其它安装或配置,就可以编写GO程序的单元测试,编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具。

在包目录内,当我们想要新建单元测试文件的时候,命名都需遵循”*_test.go”的格式,因为go test命令会遍历测试所有_test.go为后缀名的源代码文件,而且不会被go build命令编译到最终的可执行文件中。

例如下面的的目录,calc.go为源程序,calc_test.go是对应的单元测试程序,这样的文件命名一目了然

1
2
3
example/
|--calc.go
|--calc_test.go

2.go test函数种类

上面说到*_test.go文件是单元测试程序,程序里面当然就包含每一个单元测试函数,主要分为三类函数:

函数种类 格式 作用描述 示例
测试函数 函数名前缀为Test 测试程序的一些逻辑行为是否正确 func TestAdd(t *testing.T) {...}
基准函数 函数名前缀为Benchmark 测试函数的性能 func BenchmarkAdd(b *testing.B) {...}
示例函数 函数名前缀为Example 为文档提供示例文档 func ExampleAdd(t *testing.T) {...}

编写好上面的函数,然后执行go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,会在本地生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。

分享一个小技巧:当本地程序没有写测试程序的时候也可以直接执行go test命令,通过这个命令的输出内容可以快速定位当前的程序是否存在编译错误。

3. 测试函数

3.1 测试函数的格式

编写测试函数的时候必须导入testing包,测试函数的名字必须以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

3.2 测试函数编写规则

  • 测试程序文件名必须是_test.go结尾的,执行go test的时候才会执行到相应的单元测试代码

  • 必须import testing

  • 所有的测试用例函数必须是Test开头

  • 测试用例会按照源代码中写的顺序依次执行

  • 测试函数TestXxx()的参数是testing.T,我们可以使用该类型提供的方法来记录错误或者测试状态

  • 测试格式:func TestXxx (t *testing.T),Xxx部分可以为任意的字母数字的组合,但是首字母不能是小写字母[a-z],例如:Testadd是错误的函数名。

  • 函数中通过调用testing.T的Error, Errorf, Fail,FailNow, Fatal, FatalIf方法,说明测试不通过,调用Log或者Logf方法用来记录测试的信息。

3.3 测试案例编写

通过一个小案例展示如何编写单元测试和查看运行结果,程序目录结果如下:

1
2
3
example/
|--intutils.go
|--intutils_test.go

intutils.go

1
2
3
4
5
6
7
8
9
package example

func IntMin(a, b int) int {
if a < b {
return a
} else {
return b
}
}

intutils_test.go 内含两个测试函数分别是TestIntMinBasic和TestIntMinTableDriven,

后者是通过构建一个匿名结构体切片来模拟多种测试用例的情况,切片元素前两个值为需要传递的参数,最后的值为期望值,当函数返回的结果和期望值不一致,说明被测试的函数中的逻辑是有问题的,这种方式在go的单元测试编写非常常用。

除了模拟多种测试用例情况外,还用到了t.run()的方法实现子测试的运行,这样写的效果是可以清晰地看到每一个用例的执行情况,多个用例当其中出现错误的时候,通过输出的详细信息,我们就可以明确是哪一个子测试出现问题,也是一个推荐使用的小技巧

好处:

  1. 新增用例非常简单,只需给结构体切片中新增一条测试数据即可。

  2. 测试代码可读性好,直观地能够看到每个子测试的参数和期待的返回值。

  3. 用例失败时,报错信息的格式比较统一,测试报告易于阅读。

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 example

import (
"fmt"
"testing"
)

func TestIntMinBasic(t *testing.T) {
ans := IntMin(2, -2)
if ans != -2 {
t.Errorf("IntMin(2, -2) = %d; want -2", ans)
}

func TestIntMinTableDriven(t *testing.T) {
var tests = []struct {
a, b int
want int
}{
{0, 1, 0},
{1, 0, 0},
{2, -2, -2},
{0, -1, -1},
{-1, 0, -1},
}

for _, tt := range tests {
testname := fmt.Sprintf("%d,%d", tt.a, tt.b)
t.Run(testname, func(t *testing.T) {
ans := IntMin(tt.a, tt.b)
if ans != tt.want {
t.Errorf("got %d, want %d", ans, tt.want)
}
})
}
}

执行go test返回结果如下:

1
2
3
D:\mycoding\gocoding\example>go test
PASS
ok example 0.686s

如果想要查看详细的输出内容,执行go test -v,输出下面的结果,注意下子测试的写法,清晰地输出了每一个用例的执行情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
D:\mycoding\gocoding\example>go test -v
=== RUN TestIntMinBasic
--- PASS: TestIntMinBasic (0.00s)
=== RUN TestIntMinTableDriven
=== RUN TestIntMinTableDriven/0,1
=== RUN TestIntMinTableDriven/1,0
=== RUN TestIntMinTableDriven/2,-2
=== RUN TestIntMinTableDriven/0,-1
=== RUN TestIntMinTableDriven/-1,0
--- PASS: TestIntMinTableDriven (0.01s)
--- PASS: TestIntMinTableDriven/0,1 (0.00s)
--- PASS: TestIntMinTableDriven/1,0 (0.00s)
--- PASS: TestIntMinTableDriven/2,-2 (0.00s)
--- PASS: TestIntMinTableDriven/0,-1 (0.00s)
--- PASS: TestIntMinTableDriven/-1,0 (0.00s)
PASS
ok example 0.843s

上面的程序包含了两个测试函数,有时我们只想执行其中的一个函数,可以通过go test -v -run="指定函数名称或者名称子串"

1
2
3
4
5
6
7
8
9
10
11
12
D:\mycoding\gocoding\example>go test -v -run="TestIntMinBasic"
=== RUN TestIntMinBasic
--- PASS: TestIntMinBasic (0.00s)
PASS
ok example 0.803s

D:\mycoding\gocoding\example>go test -v -run="Basic"
=== RUN TestIntMinBasic
--- PASS: TestIntMinBasic (0.00s)
PASS
ok example 0.805s

尝试修改一下测试函数的期望值,比如将TestIntMinTableDriven()中最后的用例期望值修改为错误值,输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func TestIntMinTableDriven(t *testing.T) {
var tests = []struct {
a, b int
want int
}{
{0, 1, 0},
{1, 0, 0},
{2, -2, -2},
{0, -1, -1},
{-1, 0, 0}, // 修改为错误的期望值,故意错误
}

for _, tt := range tests {
testname := fmt.Sprintf("%d,%d", tt.a, tt.b)
t.Run(testname, func(t *testing.T) {
ans := IntMin(tt.a, tt.b)
if ans != tt.want {
t.Errorf("got %d, want %d", ans, tt.want)
}
})
}
}

输出结果,单元测试不通过:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
D:\mycoding\gocoding\example>go test -v -run="TestIntMinTableDriven"
=== RUN TestIntMinTableDriven
=== RUN TestIntMinTableDriven/0,1
=== RUN TestIntMinTableDriven/1,0
=== RUN TestIntMinTableDriven/2,-2
=== RUN TestIntMinTableDriven/0,-1
=== RUN TestIntMinTableDriven/-1,0
TestIntMinTableDriven/-1,0: intutils_test.go:32: got -1, want 0
--- FAIL: TestIntMinTableDriven (0.00s)
--- PASS: TestIntMinTableDriven/0,1 (0.00s)
--- PASS: TestIntMinTableDriven/1,0 (0.00s)
--- PASS: TestIntMinTableDriven/2,-2 (0.00s)
--- PASS: TestIntMinTableDriven/0,-1 (0.00s)
--- FAIL: TestIntMinTableDriven/-1,0 (0.00s)
FAIL
exit status 1
FAIL example 0.851s

3.4 子测试和表格测试的总结

根据上面的案例我们知道使用t.Run创建不同的子测试用例,主要使用方法就是传入【具体的子测试用例名称】和【对应的匿名测试函数】,然后其返回类型是布尔值,代表测试用例是否运行成功,一般情况不需要用到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// calc_test.go
func TestIntMin(t *testing.T) {
t.Run("pos", func(t *testing.T) {
if IntMin(2, 3) != 2 {
t.Fatal("fail")
}

})
t.Run("neg", func(t *testing.T) {
if IntMin(2, -3) != -3 {
t.Fatal("fail")
}
})
}

如果想要运行指定的子测试,可以通过下面的方式运行:

1
2
3
4
5
6
7
$ go test -run TestIntMin/pos -v
=== RUN TestIntMin
=== RUN TestIntMin/pos
--- PASS: TestIntMin (0.00s)
--- PASS: TestIntMin/pos (0.00s)
PASS
ok example 0.008s

但是,对于多个子测试的场景,更推荐上面用到的表格写法(table-driven tests),可以让我们集中进行用例的管理,参见上面的案例,配合for循环和t.run,当用例出现问题的时候,也能快速定位到具体的用例。

3.5 帮助函数的应用

对一些重复的逻辑,我们常常会抽取出来作为公共的帮助函数(helpers),可以增加测试代码的可读性和可维护性。 借助帮助函数,可以让测试用例的主逻辑看起来更清晰,减少代码的重复性,但是如果像下面的案例,故意让最后的case出现错误,那么go test运行后的输出结果只能告诉我们是createIntMinCase函数里面的第2行代码的if判断出现了错误,这样我们就无法直接知道下面三个case中哪一个出现错误了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type intMinCase struct{ A, B, Expected int }

func createIntMinCase(t *testing.T, c *intMinCase) {
//t.Helper()
if ans :=IntMin (c.A, c.B); ans != c.Expected {
t.Fatalf("%d * %d expected %d, but %d got",
c.A, c.B, c.Expected, ans)
}
}

func TestIntMinHelper(t *testing.T) {
createIntMinCase(t, &intMinCase{1, 0, 0})
createIntMinCase(t, &intMinCase{2, -3, -3})
createIntMinCase(t, &intMinCase{2, 0, -1}) // wrong case
}

输出结果如下,只知道是公共函数里面报错了:

1
2
3
4
5
6
7
8
D:\mycoding\gocoding\example>go test -v -run="TestIntMinHelper"
=== RUN TestIntMinHelper
TestIntMinHelper: intutils_test.go:43: 2 * 0 expected 1, but 0 got
--- FAIL: TestIntMinHelper (0.00s)
FAIL
exit status 1
FAIL example 0.540s

那么,如果知道是哪一个case出现了错误的判断呢?

Go 语言在 1.9 版本中引入了 t.Helper(),用于标注该函数是帮助函数,报错时将输出帮助函数调用者的信息,而不是帮助函数的内部信息。

所以上面的案例,我们只需要在createIntMinCase函数里面写上t.Helper()再执行go test只能知道是一个case出错,如下输出的结果:
上面没加t.Helper()报错的43行,加后输出的结果是51行,根据代码的行号明确了是第三个case出现了错误

1
2
3
4
5
6
7
8
D:\mycoding\gocoding\example>go test -v -run="TestIntMinHelper"
=== RUN TestIntMinHelper
TestIntMinHelper: intutils_test.go:51: 2 * 0 expected 1, but 0 got
--- FAIL: TestIntMinHelper (0.00s)
FAIL
exit status 1
FAIL example 0.521s

3.6 setup 和 teardown

如果在同一个测试文件中,每一个测试用例运行前后的逻辑是相同的,也就是存在公共的调用函数,一般会写在setupteardown函数中。例如执行前需要实例化或者初始化待测试的对象,并且如果这个对象比较复杂,很适合将这一部分逻辑提取出来;
执行测试逻辑后,甚至我们可能会做一些资源回收类的工作,例如关闭网络连接,释放文件等操作。标准库testing提供了这样的机制:

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 example

import (
"fmt"
"os"
"testing"
)

func setup() {
fmt.Println("Before all tests")
}

func teardown() {
fmt.Println("After all tests")
}

func Test1(t *testing.T) {
fmt.Println("I'm test1")
}

func Test2(t *testing.T) {
fmt.Println("I'm test2")
}

func TestMain(m *testing.M) {
setup()
code := m.Run()
teardown()
os.Exit(code)
}
  • 该测试文件中,包含有2个测试用例Test1Test2

  • 如果测试文件中包含函数 TestMain,那么生成的测试将先调用TestMain(m),而不是直接运行测试

  • 然后在其中调用m.Run()触发所有测试用例的执行,并使用os.Exit()处理返回的状态码,如果不为0,说明有用例失败s

  • 因此可以在调用m.Run()前后做一些额外的准备(setup)和回收(teardown)工作

输出的运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
D:\mycoding\gocoding\example>go test -v demo_test.go
Before all tests
=== RUN Test1
I'm test1
--- PASS: Test1 (0.00s)
=== RUN Test2
I'm test2
--- PASS: Test2 (0.00s)
PASS
After all tests
ok command-line-arguments 0.561s

3.7 http单元测试

有些时候我们需要验证某个API接口的handler是否能正常工作,除了直接mock数据去调用handler内部的业务函数外,还可以自己编写htpp请求一次性构造测试测试,达到更直接的测试作用

例如下面一个简单的handler函数:

1
2
3
func helloHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello golang"))
}

3.7.1 简约版

我们可以直接使用htppnet库在测试函数中构造http服务和请求来进行handler的功能验证

  1. 利用net.Listen("tcp", "127.0.0.1:0")监听一个未被占用的端口,并返回Listener

  2. 另起携程调用http.Serve(ln, nil)启动http监听服务

  3. 使用http.Get发起一个Get请求,检查返回值是否正确

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 handleError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatal("failed", err)
}
}

func TestConn(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
handleError(t, err)
defer ln.Close()

http.HandleFunc("/hello", helloHandler)
go http.Serve(ln, nil)

resp, err := http.Get("http://" + ln.Addr().String() + "/hello")
handleError(t, err)

defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
handleError(t, err)
log.Printf("resp:%s",string(body))
if string(body) != "hello golang" {
t.Fatal("expected hello world, but got", string(body))
}
}

运行后的结果如下:

1
2
3
4
5
6
7
8
9
D:\mycoding\gocoding\example>go test -v -test.run=TestConn
Before all tests
=== RUN TestConn
2021/07/28 23:35:22 resp:hello golang
--- PASS: TestConn (0.02s)
PASS
After all tests
ok example 0.805s

3.7.2 高效版

针对http开发的场景,使用标准库net/http/httptest可以进行更为高效的测试,使用httptest模拟请求对象(req)和响应对象(w),达到了相同的目的

使用httptest改写上面的案例:

1
2
3
4
5
6
7
8
9
10
func TestConnByHttpTest(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com/foo", nil)
w := httptest.NewRecorder()
helloHandler(w, req)
body, _ := ioutil.ReadAll(w.Result().Body)
log.Printf("resp:%s",string(body))
if string(body) != "hello golang" {
t.Fatal("expected hello world, but got", string(body))
}
}

输出结果如下:

1
2
3
4
5
6
7
8
9
D:\mycoding\gocoding\example>go test -v -test.run=TestConnByHttpTest
Before all tests
=== RUN TestConnByHttpTest
2021/07/28 23:55:42 resp:hello golang
--- PASS: TestConnByHttpTest (0.03s)
PASS
After all tests
ok example 0.779s

4.测试覆盖率

当我们写完单元测试的时候,为了查看是否测试覆盖是否足够完整,都会查看一下测试覆盖率,其中go test同样提供了便捷的工具供我们使用

Go提供内置功能来检查你的代码覆盖率。我们可以使用go test -cover来查看测试覆盖率。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
D:\mycoding\gocoding\example>go test -cover
--- FAIL: TestIntMinBasic (0.00s)
intutils_test.go:11: IntMin(2, -2) = -2; want -2
--- FAIL: TestIntMinTableDriven (0.00s)
--- FAIL: TestIntMinTableDriven/-1,0 (0.00s)
intutils_test.go:32: got -1, want 0
--- FAIL: TestIntMinHelper (0.00s)
intutils_test.go:51: 2 * 0 expected 1, but 0 got
FAIL
coverage: 100.0% of statements
exit status 1
FAIL example 0.672s

根据上面的输出结果可以看出我们的测试覆盖率是达到了100%,当然真实的开发情况不可能这么简单,通过查出的覆盖率,可以让我们查缺补漏补充对应的单例,提高覆盖率。

另外,除了上面的查看方式,go test还提供了快捷生成HTML报告的命令,具体的做法是:

  1. 提供了一个额外的-coverprofile参数,用来将覆盖率相关的记录信息输出到一个文件,比如c.out

  2. 执行go tool cover -html=c.out,使用cover工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成一个HTML报告

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
D:\mycoding\gocoding\example>go test -cover -coverprofile=c.out
--- FAIL: TestIntMinBasic (0.00s)
intutils_test.go:11: IntMin(2, -2) = -2; want -2
--- FAIL: TestIntMinTableDriven (0.00s)
--- FAIL: TestIntMinTableDriven/-1,0 (0.00s)
intutils_test.go:32: got -1, want 0
--- FAIL: TestIntMinHelper (0.00s)
intutils_test.go:51: 2 * 0 expected 1, but 0 got
FAIL
coverage: 100.0% of statements
exit status 1
FAIL example 0.649s

D:\mycoding\gocoding\example>go tool cover -html=c.out

浏览器输出测试覆盖情况,绿色代表覆盖,红色代表未覆盖,灰色代表不需要跟踪代码。

测试覆盖率

5.基准测试

5.1 基准测试格式

基准测试的基本格式如下:

1
2
3
func BenchmarkName(b *testing.B){
// ...
}
  • 函数名必须以Benchmark开头,后面一般跟待测试的函数名
  • 参数为b *testing.B
  • 执行基准测试时,需要添加-bench参数
  • 基准测试必须要执行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()

5.2 基准测试案例

1
2
3
4
5
func BenchmarkHello(b *testing.B) {
for i := 0; i < b.N; i++ {
fmt.Sprintf("hello")
}
}

执行基准测试可以通过go test -benchmem -bench .运行所有的基准测试案例,也可以指定运行某个案例函数名称,go test -benchmem -bench=Hello,其中-benchmem指定获取内存分配的统计数据。

1
2
3
4
5
6
7
D:\mycoding\gocoding\example>go test -benchmem -bench .
goos: windows
goarch: amd64
pkg: example
BenchmarkHello-8 6628830 170 ns/op 5 B/op 1 allocs/op
PASS
ok example 2.060s

其中BenchmarkSplit-8表示对Hello函数进行基准测试,数字8表示GOMAXPROCS的值,这个对于并发基准测试很重要。6628830170ns/op表示每次调用Hello函数耗时203ns,这个结果是6628830次调用的平均值,5 B/op表示每次操作内存分配了112字节1 allocs/op则表示每次操作进行了1次内存分配。

基准测试报告每一列值对应的含义如下:

1
2
3
4
5
6
7
type BenchmarkResult struct {
N int // 迭代次数
T time.Duration // 基准测试花费的时间
Bytes int64 // 一次迭代处理的字节数
MemAllocs uint64 // 总的分配内存的次数
MemBytes uint64 // 总的分配内存的字节数
}

5.3 性能比较函数

上面的基准测试只能得到给定操作的绝对耗时,但是在很多性能问题是发生在两个不同操作之间的相对耗时,比如同一个函数处理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
// fib.go

// Fib 是一个计算第n个斐波那契数的函数
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
// 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) }

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
D:\mycoding\gocoding\example>go test -benchmem -bench .
goos: windows
goarch: amd64
pkg: example
BenchmarkFib1-8 339907020 4.30 ns/op 0 B/op 0 allocs/op
BenchmarkFib2-8 150234828 12.1 ns/op 0 B/op 0 allocs/op
BenchmarkFib3-8 47958372 23.4 ns/op 0 B/op 0 allocs/op
BenchmarkFib10-8 1880780 616 ns/op 0 B/op 0 allocs/op
BenchmarkFib20-8 9999 110119 ns/op 0 B/op 0 allocs/op
BenchmarkFib40-8 1 1598119000 ns/op 0 B/op 0 allocs/op
PASS
ok example 11.504s

这里需要注意的是,默认情况下,每个基准测试至少运行1秒。如果在Benchmark函数返回时没有到1秒,则b.N的值会按1,2,5,10,20,50,…增加,并且函数再次运行。

最终的BenchmarkFib40只运行了一次,像这种情况下我们应该可以使用-benchtime标志增加最小基准时间,以产生更准确的结果。例如:

1
2
3
4
5
6
7
8
D:\mycoding\gocoding\example>go test -benchmem -bench=Fib40 -benchtime=20s
goos: windows
goarch: amd64
pkg: example
BenchmarkFib40-8 22 1513385245 ns/op 0 B/op 0 allocs/op
PASS
ok example 35.124s

这一次BenchmarkFib40函数运行了22次,结果性对会更准确一些了。

5.4 重置定时器

如果在运行前基准测试需要一些耗时的配置,则可以使用b.ResetTimer()先重置定时器,例如:

1
2
3
4
5
6
7
func BenchmarkHello(b *testing.B) {
... // 耗时操作
b.ResetTimer()
for i := 0; i < b.N; i++ {
fmt.Sprintf("hello")
}
}

5.5 并行测试

func (b *B) RunParallel(body func(*PB))会以并行的方式执行给定的基准测试。

RunParallel会创建出多个goroutine,并将b.N分配给这些goroutine执行, 其中goroutine数量的默认值为GOMAXPROCS
用户如果想要增加非CPU受限(non-CPU-bound)基准测试的并行性, 那么可以在RunParallel之前调用SetParallelismRunParallel通常会与-cpu标志一同使用。

1
2
3
4
5
6
7
8
9
10
11
12
func BenchmarkParallel(b *testing.B) {
templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
// b.SetParallelism(4) // 设置使用的CPU数
b.RunParallel(func(pb *testing.PB) {
var buf bytes.Buffer
for pb.Next() {
// 所有 goroutine 一起,循环一共执行 b.N 次
buf.Reset()
templ.Execute(&buf, "World")
}
})
}

运行结果如下:

1
2
3
4
5
6
7
8
D:\mycoding\gocoding\example>go test -benchmem -bench=BenchmarkParallel
goos: windows
goarch: amd64
pkg: example
BenchmarkParallel-8 12807045 82.9 ns/op 48 B/op 1 allocs/op
PASS
ok example 2.883s

微信公众号

扫一扫关注Joyo说公众号,共同学习和研究开发技术。

weixin-a

评论