本文变更日志:
2021-09-09 更新,基于 Go 1.16.7 更新
改动点:
- 完善了表组测试的说明
- 修改了基准测试,1.16 版本下,不再需要主动设置 b.N
对于有经验的工程师,单元测试对于系统而言的重要性不言而喻。尤其是当某一段代码“牵一发而动全身”时,单测可以高效的回归功能,提高系统的稳定性和研发效率。
一般的单元测试,有以下几种:
- 基础测试,只使用一组参数和结果来测试一段代码。表组测试,相比较基础单测,采用多组参数和结果测试一段代码
- 基准 benchmark 测试,用于测试程序性能,主要通过测试 CPU 和内存,来评估性能
- Mock 测试,模拟网络和数据库环境,进行测试。一段标准的单元测试,应该是与环境无关的,在执行单测时,不该请求网络或 DB,因此请求网络和 DB 的代码,需要通过 Mock 来模拟
Go lang 对单元测试的支持是相当完备的,相比较 Java 需要思考在 JUnit 还是 TestNg 的选型,Go 的标准库 testing
就提供了单元测试的能力。
使用 testing
标准库,Go lang 需严格准守这 “约定大于配置” 的规则,只有遵守约定,测试工具才会将其视为单元测试进行执行。
- 文件名必须以
_test.go
结尾 - 必须
import "testing"
基础测试
使用 Golang 的标准库 testing
进行基础测试
- 测试用例函数入参必须为
t *testing.T
,并且无返回 - 测试用例函数必须以
Test
作为开头
测试 addNum
函数
func addNum(a, b int) int {
return a + b
}
示例如下:
import "testing"
func Test_addNum1(t *testing.T) {
addNum(1,2)
t.Log("ok")
}
testing
标准库下,通过日志来表明单测成功、失败
t.Log
系列为测试正常输出,若仅有t.Log
输出,则表示测试通过
t.Error
系列不会终止测试函数运行,但会在结果中显示为测试函数执行错误
t.Fatal
系列会终止当前测试函数运行,进入下一个测试函数
如何执行单测呢?
命令行下
命令行多种方式,可以指定执行某个 _test.go 下的单测。
也可以按 package 或 directory 维度,执行 package 或 directory 下所有单测。
需要注意的是,如果执行单测,需要使得单测依赖的函数,都被引用。如下:
go test common/add_num_test.go
# 错误
# command-line-arguments [command-line-arguments.test]
common/add_num_test.go:11:2: undefined: addNum
common/add_num_test.go:35:14: undefined: addNum
FAIL command-line-arguments [build failed]
FAIL
# 正确
go test common/add_num_test.go common/add_num.go
go test common/add_num* -v -run=^Test_addNum1$
# 结果为
=== RUN Test_addNum1
add_num_test.go:10: ok
--- PASS: Test_addNum1 (0.00s)
PASS
coverage: 100.0% of statements
ok command-line-arguments 0.293s coverage: 100.0% of statements
或者直接指定 package 或 directory,如
go test ./...
结果为:
? go-test [no test files]
--- FAIL: Test_addNum (0.00s)
--- FAIL: Test_addNum/3 (0.00s)
add_num_test.go:32: addNum() = 0, want 2
FAIL
FAIL go-test/common 0.477s
FAIL
IDE 下
采用的是 Goland IDE
Goland 下配置单元测试截图限定了 package 以及要执行的函数(pattern 限定),如果要执行 package 下所有单测,则去掉 pattern 配置即可。
单元测试参数配置
一般常用配置,有
-
-v
,表示 verbose,不带 -v 的话,仅展示 FAIL 的 case -
-run
, 表示要执行的函数的正则表达,如 -run=^Test_addNum1$ -
-cover
,表示是否返回测试覆盖率
表组测试
在准备单测用例时,经常需要对一个函数进行多种输入的测试,如果写多个 Test 函数,会比较麻烦,因此 Go 提供了表组测试的实践,可以一次性提供多种输入。
表组测试,本质上,就是用循环来跑一堆基本测试。
用一个切片存储一组输入参数,并通过迭代执行该切片所有输入。
示例代码:
import (
"fmt"
"strconv"
"testing"
)
func Test_addNum(t *testing.T) {
type args struct {
a int
b int
}
tests := []struct {
name string
args args
want int
}{
// TODO: Add test cases.
{"1", args{1, 2}, 3},
{"2", args{0, 0}, 0},
{"3", args{0, 0}, 2},
{"4", args{0, -1}, -1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := addNum(tt.args.a, tt.args.b); got != tt.want {
t.Errorf("addNum() = %v, want %v", got, tt.want)
}
})
}
}
基准测试
基准测试是测试性能的方法,可以识别某段代码的 CPU 或内存效率,或者辅助配置工作池数量,以提高吞吐。
基准测试与单元测试在使用上大同小异。
- 测试用例函数必须以
Benchmark
作为开头 - 测试用例函数入参必须为
t *testing.B
,并且无返回 -
b.RestTimer()
重置计时器,在 reset 前编写初始化代码,避免初始化代码对基准测试的干扰 - 基准测试代码,放到
for
循环内 -
b.N
由基准测试框架提供,无法再设置,设置会造成基准测试执行超时。
import (
"testing"
)
func BenchmarkItoa(b *testing.B) {
number := 10
b.ResetTimer()
for i := 0; i < b.N; i++ {
strconv.Itoa(number)
}
}
如何执行基准单测呢?
命令行下执行
一般命令行执行基准测试,命令如下:
go test common/* -bench=. -run=none -benchtime=1s -benchmem -parallel=1 -timeout=1s
输出如下:
其中,
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
BenchmarkAddNum1-16 164469 7233 ns/op 0 B/op 0 allocs/op
BenchmarkAddNum2-16 164374 7271 ns/op 0 B/op 0 allocs/op
BenchmarkAddNum3-16 166478 7238 ns/op 0 B/op 0 allocs/op
PASS
输出每列:
- 第 1列,BenchmarkAddNum1-16,- 前面为基准测试函数,
16
为 Go lang 使用的 CPU 核数,可以通过 Go env 的GOMAXPROCS
设置,默认为机器 CPU 核数。 - 第 2 列,表示在基准测试周期内
for
循环的次数。由于命令指定了-benchtime=1s
, 也就是说,基准测试执行周期为 1s,也就是说在 1s 内,执行了 16w+ 次 for 循环 - 第 3 列,单位 ns/op 表示每条指令消耗的纳秒数
- 第 4 列,B/op 表示每条指令消耗的内存
- 第 5 列,allocs/op 表示每条执行分配内存次数
基准测试参数配置
更多的测试参数配置,可以通过 go help testflag
查看。一般的配置有:
-
-bench regexp
,指定要执行的基准测试的函数正则,如果要执行所有的,则为 -bench=. -
-benchtime 5s
,表示每个基准测试至少跑 5s,默认 1s。如果要指定跑的次数,为 -benchtime=1000x,表示执行 1000 次 -
-count n
,表示每个基准测试函数执行次数,默认 1次。 -
-cpu n
,指定用多少 CPU 跑测试,默认为机器核数,若设置了GOMAXPROCS
,则用GOMAXPROCS
-
-benchmem
显示内存消耗 -
-run regexp
,指定要跑的基础单元测试,一般跑基准测试时,会排除跑单元测试,会设置为 -run=^$ -
-parallel 4
,指定并发 -
timeout 120s
若测试跑了超过 120s 则退出
参考资料
[1] 《Go语言程序设计》(The Go Programming Language)
网友评论