Go语言学习--测试(Go Test)
# 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
}
}
2
3
4
5
6
7
8
9
10
11
12
13
其中,testing.T
有几个函数可以帮助输出日志。
t.Log/Logf # 日志信号
t.Fail/Failf # 失败信号,测试继续
t.FailNow # 失败信号,测试终止
2
3
其中,将日志和测试信号组合,可以提供如下函数。
t.Log + t.Fail = t.Error
t.Log + t.FailNow = t.Fatal
2
# Go test 运行方式
go test 有两种运行方式
# 1. 不带参数,运行本地文件下的测试文件
go test
# 2. 指定确定的包及其依赖
go test addTest.go, pkg/util/math, ...
# 依赖包太多,建议直接使用 go test
2
3
4
5
go test 的常用 flag
有
go test
-args 命令行参数
-v 详细模式运行
-parallel 并行测试
-run 指定测试函数, 这里默认使用了正则表达式 -run "Add4$"
-count 重复测试次数, 默认 1
-timeout 全部累计测试时间
2
3
4
5
6
7
更多详细,参考
go help test
和go 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)
}
2
3
4
5
6
7
8
9
10
11
12
# 数据表驱动的测试
数据测试需要考虑到多种情况,可以通过数据表来批量测试
// 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)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 跳过长任务
通过 -short
和 testing.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)
}
2
3
4
5
6
7
8
9
# 任务管理
有时,我们希望在测试的开始(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)
}
2
3
4
5
6
7
8
9
10
# 更加定制化的管理
go test
在运行的时候,其实会调用 m.Run()
方法,我们可以不用内置的处理流程,可以自定义方法。
主要通过两个函数 testing.Main
和 testing.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)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 测试文件的组织&以测试驱动的编码
在单元测试中,函数命名需要遵循 TestXxx
的规则,自动化测试会寻找
此外,测试文件的命名和组织也有要求,测试文件需要和程序处于同一 文件夹
和 package
下,文件名加上 _test
,这样 Go 在编译的时候就会自动跳过。
当测试文件与原始文件在同一包下的时候,如何写出一个合适的,可测试的代码就显得比较重要,需要程序员提前对程序进行设计思考,写出以测试为驱动的代码。做到程序功能上的解耦。
以 gohugoio/hugo (opens new window) 为例,可以看到几乎任何一个 .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
...
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
# 基准/性能测试 Benchmarks
Benchmarks 主要从如下方面测试程序性能:
- 运行时间分析
- 运行内存分析
# 最简单的例子
// Simple Example
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Add(1, 2)
}
}
2
3
4
5
6
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
2
3
4
5
6
7
8
9
10
11
12
13
结果解释
- 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
2
3
4
5
6
7
8
也可以使用特殊语法 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
2
3
4
5
6
7
8
# 运行时间控制
有时测试中会包含一些耗时的程序,如果都从 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++
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 运行内存分析
func heap() []byte {
return make([]byte, 1024)
}
func Benchmark_Alloc(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = heap()
}
}
2
3
4
5
6
7
8
9
10
$ 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
2
3
4
5
6
7
8
9
10
注意,这里除了加上 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
}
2
3
4
5
6
7
8
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
}
2
3
4
5
6
7
8
9
10
11
# 学习案例
百学不如一练,可以通过观察开源项目中 go test
的具体使用。