引言

Go提供了testing包用来进行单元测试和基准测试(benchmark),使用go test指令运行对应目录下的测试用例,并输出测试结果。

单元测试

一个例子引入,文件名为some_func_test.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import "testing"

func Echo(str string) string {
	return str
}

func TestEcho(t *testing.T){
    str := "来♂"
	if Echo(str) != str {
		t.Errorf("Echo fail\n")
	}
}

some_func_test.go文件的父级目录下打开shell,执行go test,则会运行测试用例TestEcho()

  • 测试文件的命名格式为xxx_test.go, 如本例子为some_func_test.go
  • 测试用例命名规则为TestXXX, 如例子中的TestEcho(t *testing.T)
  • 测试需要用到的包是testing
    • testing.T用于普通测试用例
    • testing.M用于TestMain测试用例
    • testing.B用于基准测试(benchmark)
  • go test其他参数
    • go test -v 会显示每个测试用例的结果
    • go test -cover 会显示覆盖率
    • go test -run TestAdd-run指定运行的测试用例, 支持正则表达式

子测试集

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
	"fmt"
	"testing"
)

func TestWalk(t *testing.T){
	left := false
	right := false
	t.Run("StepLeftLeg", func(t *testing.T){
		left = true
	})
	t.Run("StepRightLeg", func(t *testing.T){
		right = true
	})
	if left && right {
		fmt.Println("ok!")
	}else {
		t.Fatalf("Your leg is hurt")  // Fatal()和Error()的区别在于,Fatal()会暂停后续测试,而Error()不会
	}
}

testing.Msetupteardown

测试时可能需要申请资源以构建出指定的运行环境,如建立数据库连接,可以将其抽象在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
package main

import (
	"fmt"
	"testing"
)

func setup(){
	fmt.Println("setup...")
}

func teardown(){
	fmt.Println("teardown...")
}

func Say(str string) string {
	return str
}

func TestSay(t *testing.T) {
	hello := "hello"
	if Say(hello) != hello {
		t.Fatalf("say hello fail\n")
	}
}

func TestMain(m *testing.M){
	setup()
	m.Run()
	teardown()
}

基准测试(benchmark)

一个例子引入, benchmark_test.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import (
	"testing"
)

func BenchmarkEcho(b *testing.B) {
	arr := []byte{}
	for i:=0; i<b.N; i++ {
		arr = append(arr, "来♂"...)
	}
}


运行命令:go test -benchmem -bench ., 表明运行当前目录下的benchmark。输出:

1
2
3
4
5
6
7
8
goos: linux
goarch: amd64
pkg: playground
cpu: AMD Ryzen 7 5800X 8-Core Processor             
BenchmarkEcho-2         210461787                6.279 ns/op          36 B/op          0 allocs/op
PASS
ok      playground      2.519s

其中:

  • 文件命名格式为:xxx_test.go

  • benchmark函数的函数名格式为:BenchmarkXxxx,例如BenchmarkEcho(b *testing.B), Echo首字母大写

  • b.N为运行次数,这个次数是1秒内的迭代次数,benchmark默认运行1秒

  • BenchmarkEcho-2中的2指的是GOMAXPROCS, 为CPU核数

  • 210461787表示的是单位时间1秒内运行的次数

  • 6.279 ns/op指的是每次迭代花费6.279 ns

  • 210461787 * 6.279 != 2.519s 是因为创建和销毁的时耗没有包括在其中

  • 36 B/op 指的是每次迭代平均分配(allocated)36 Byte

  • 0 allocs/op 指的是每次迭代中平均有多少个不同的内存分配 how many distinct memory allocations occurred per op (single iteration)

  • -bench是执行基准测试必须的命令行选项, go test和benchmark相关的其他命令行选项

    • -benchmem 添加额外的内存信息
    • -benchtime指定运行时间或运行次数,例如go test -benchtime=5s -bench .指定时间, go test -benchtime=1000x -bench 指定op为100次数
    • -count 指定benchmark运行次数,例如go test -bench . -count=3
    • -cpu指定cpu数,例如go test -bench . cpu 1,2,4,8, 分别以cpu=1,2,4,8进行基准测试

benchmark计时器、ResetTimer()、StopTimer()、StartTimer()

计时器相关的函数和方法可以使的时间计算更加灵活,参考伪代码:

1
2
3
4
5
6
7
8
9
func BenchmarkSometest(b *testing.B){
    init()
    b.ResetTimer()
    benchmark1()
    b.StopTimer()
    doOtherThing()
    b.StartTimer()
    benchmark2()
}

并行benchmark

1
2
3
4
5
6
7
func BenchmarkEchoParallel(b *testing.B){
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next(){
            SomeBenchmark()
        }
    })
}
  • 并行benchmark的原理是,将b.N次迭代分散到P个Goroutine上。
  • 使用func (b *B) SetParallelism(p int) 可以设置RunParallel()使用的Goroutine的个数, 计算方法为p*GOMAXPROCS
  • 对于CPU密集的测试没必要使用SetParallelism()

测试用例写法之表驱动

和pytest类似