跳到主要内容

Go 语言学习--测试(Go Test)

Go 语言可以很方便的让我们完成测试,通过借助 testing 包,很容易搭建一个测试框架。

测试包含三个部分:

  • 单元测试
  • 性能测试
  • 样例测试

单元测试

单元测试(unit testing)是指对程序中的最小可测试单元进行检查和验证。通常单元测试针对的是程序中的一个函数或者一段方法。

判断程序是否为一个 单元 的方法是其能否独立的进行测试

最简单的单元测试

func Add(x, y int) int {
return x + y
}

// Simple Testing
func TestAdd(t *testing.T) {
if Add(1, 2) != 3 {
t.Log("log: add function success") // 仅失败 或 -v 输出
t.FailNow()
//t.Error() // Log -> Fail
//t.Fatal() // Log -> FailNow
}
}

其中,testing.T 有几个函数可以帮助输出日志。

t.Log/Logf   # 日志信号 
t.Fail/Failf # 失败信号,测试继续
t.FailNow # 失败信号,测试终止

其中,将日志和测试信号组合,可以提供如下函数。

t.Log + t.Fail = t.Error
t.Log + t.FailNow = t.Fatal

Go test 运行方式

go test 有两种运行方式

# 1. 不带参数,运行本地文件下的测试文件
go test
# 2. 指定确定的包及其依赖
go test addTest.go, pkg/util/math, ...
# 依赖包太多,建议直接使用 go test

go test 的常用 flag

go test 
-args 命令行参数
-v 详细模式运行
-parallel 并行测试
-run 指定测试函数, 这里默认使用了正则表达式 -run "Add4$"
-count 重复测试次数, 默认 1
-timeout 全部累计测试时间

更多详细,参考 go help testgo help testflags 来获取 go test 命令和 flag 的帮助

并行测试

通过 --parallel n 指定并行测试个数,通过 t.Parallel 来控制

// understanding t.Parallel
// TestA and TestB will run the same time
// go test -parallel 2
func TestA(t *testing.T) {
t.Parallel()
time.Sleep(time.Second * 2)
}

func TestB(t *testing.T) {
t.Parallel()
time.Sleep(time.Second * 2)
}

数据表驱动的测试

数据测试需要考虑到多种情况,可以通过数据表来批量测试

// table driven test
func Add2(x, y int) int {
return x + y
}

func TestAdd2(t *testing.T) {
fmt.Printf("start test TestAdd2")
var test = []struct{
x, y, expect int
}{
{1, 1, 2},
{2, 2, 4},
{5, 3, 8},
}

for _, tt := range test {
if actual := Add(tt.x, tt.y); actual != tt.expect {
t.Errorf("add(%d, %d), expect: %d, actual:%d", tt.x, tt.y, tt.expect, actual)
}
}
}

跳过长任务

通过 -shorttesting.Short 配合,可以跳过执行耗时较长的任务。

// skip long runing test
// go test -short
func TestAdd3(t *testing.T) {
fmt.Printf("start test TestAdd3")
if testing.Short() {
t.Skip("Skip long runing test")
}
time.Sleep(100 * time.Second)
}

任务管理

有时,我们希望在测试的开始(setup)和结束(teardown)做一些操作,比如常见的有数据库的连接和断开,这时可以通过 TestMain 来管理,go test 会自动寻找 TestMain 作为入口。

// sometime wo need to do some necessary thing before or after testing, like connect and disconnect database.
// or wo need to control which code should run in main thred.
func TestMain(m *testing.M){
// setup
fmt.Println("before test")
code := m.Run() // call test routine func
// teardown
fmt.Println("after test")
os.Exit(code)
}

更加定制化的管理

go test 在运行的时候,其实会调用 m.Run() 方法,我们可以不用内置的处理流程,可以自定义方法。

主要通过两个函数 testing.Maintesting.MainStart

输入的参数为:

  • matchString:flags 参数
  • tests:需要运行的单元测试
  • benchmarks:需要运行的性能测试
  • examples:需要运行的样例测试

这一块,在看代码的时候发现 testing 库的变动很大,所以目前没有什么比价好的教程和文档,具体实现需要自主参考源码。

其中, testing.MainStart 处于最底层,需要实现 testing.M 对象,实现起来较为复杂。

// Control each test, benchmarks, examples
func TestMain(m *testing.M) {
// 这里是有坑,testing包经过升级改动,原先的MainStart就是现在的Main
// testing.Main 使用的特点是,可以有setup初始化,但是teardown不能自主控制
matchString := func(pat, str string) (bool, error) {
return true, nil
}

tests := []testing.InternalTest{
{"TestAdd", TestAdd},
{"TestAdd2", TestAdd2},
}

benchmarks := []testing.InternalBenchmark{}
examples := []testing.InternalExample{}

testing.Main(matchString, tests, benchmarks, examples)
}

测试文件的组织&以测试驱动的编码

在单元测试中,函数命名需要遵循 TestXxx 的规则,自动化测试会寻找

此外,测试文件的命名和组织也有要求,测试文件需要和程序处于同一 文件夹package 下,文件名加上 _test,这样 Go 在编译的时候就会自动跳过。

当测试文件与原始文件在同一包下的时候,如何写出一个合适的,可测试的代码就显得比较重要,需要程序员提前对程序进行设计思考,写出以测试为驱动的代码。做到程序功能上的解耦。

gohugoio/hugo 为例,可以看到几乎任何一个 .go 文件都有对应的 _test.go 测试文件,并且处于同一目录下。

.
├── compare
│   ├── compare.go
│   ├── compare_test.go
│   ├── init.go
│   └── init_test.go
├── crypto
│   ├── crypto.go
│   ├── crypto_test.go
│   ├── init.go
│   └── init_test.go
├── data
│   ├── data.go
│   ├── data_test.go
│   ├── init.go
│   ├── init_test.go
│   ├── resources.go
│   └── resources_test.go
├── debug
│   ├── debug.go
│   ├── init.go
│   └── init_test.go
├── encoding
│   ├── encoding.go
│   ├── encoding_test.go
│   ├── init.go
│   └── init_test.go
├── fmt
│   ├── fmt.go
│   ├── init.go
│   └── init_test.go
├── hugo
│   ├── init.go
│   └── init_test.go
...

基准/性能测试 Benchmarks

Benchmarks 主要从如下方面测试程序性能:

  • 运行时间分析
  • 运行内存分析

最简单的例子

// Simple Example
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Add(1, 2)
}
}

go test 执行测试

$ go test -v -run=None -bench=Add2  benchmark_test.go unit_test.go
-v 详细日志
-run regexp 正则模式,. 表示所有 none 表示不运行 Test和Example
-bench regexp 正则模式,同上


goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
BenchmarkAdd2
BenchmarkAdd2-12 1 2000631650 ns/op
PASS
ok command-line-arguments 7.020s

结果解释

  • BenchmarkAdd2-12 中的12是指,在调用CPU的使用默认调用GOMAXPROCS ,可以通过 -cpu N 来指定使用的CPU核数
  • 2000631650 ns/op 表示每个操作花费 2000631650纳秒。
  • 7.020s 表示执行的总花费时间,从代码可以看出 2 + 5 =7s

一点点需要主要的地方

go test 运行带 flags 参数的话,测试代码中不能包含 TestMain 函数。

自定义测试时间内

上面的例子可以看出,测试程序只运行了1次,这对于我们测试结果有影响,一般多次测试取平均所得结果较为准确。运行一次的原因在于,go test-benchtime=1s 默认为1秒。

$ go test -v -run=None -bench=Add2 -benchtime=10s benchmark_test.go unit_test.go
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
BenchmarkAdd2
BenchmarkAdd2-12 1000000000 2.001 ns/op
PASS
ok command-line-arguments 91.253s

也可以使用特殊语法 Nx ,N代表最小执行的次数。

$ go test -v -run=None -bench=Add2 -benchtime=10x benchmark_test.go unit_test.go
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
BenchmarkAdd2
BenchmarkAdd2-12 10 200507066 ns/op
PASS
ok command-line-arguments 14.046s

运行时间控制

有时测试中会包含一些耗时的程序,如果都从 Benchmark() 开始计时会使得测试结果不准确,go 中可以通过 testing.B 只针对需要测试的代码计时。

// timer control
// go test -run=none -v -bench BenchmarkAdd2 benchmark_test.go unit_test.g
func BenchmarkAdd2(b *testing.B) {
b.ResetTimer() // reset timer
sleep(2)

var n int
for i := 0; i < 5; i++ {
b.StopTimer() // stop timer
sleep(1)
b.StartTimer() // start timer
n++
}
}

运行内存分析

func heap() []byte {
return make([]byte, 1024)
}

func Benchmark_Alloc(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = heap()
}
}

$ go test -v -run=None -bench=Alloc -benchmem 
benchmark_test.go unit_test.go -gcflags "-N -l"

goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
Benchmark_Alloc
Benchmark_Alloc-12 475332 2388 ns/op 1024 B/op 1 allocs/op
PASS
ok command-line-arguments 2.095s

注意,这里除了加上 benchmem 外,还需要加上 -gcflags "-N -l"关闭内联优化,不然 B/op 总为0。

样例 Example

testing 包同样可以用于验证样例代码,与之前相同,代码必须以 ExampleXxx 命名。

简单的样例

// Simple Example
func ExampleAdd(){
fmt.Println(Add(1,2))
fmt.Println(Add(2,2))
// Output:
// 3
// 4
}

go test 会比较注释中 Output 后的输出

无序的输出

输出并不一定是有序的,同样go test 会比较注释中的无序输出。此时注释前为 Unordered output

// Unorder output
func ExamplePerm() {
for _, value := range rand.Perm(5) { // Example must use Perm method
fmt.Println(value)
}
// Unordered output: 4
// 2
// 1
// 3
// 0
}

学习案例

百学不如一练,可以通过观察开源项目中 go test 的具体使用。

参考资料