0

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

 2 years ago
source link: https://ppsteven.github.io/pages/48be02/#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13

其中,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 
1
2
3
4
5

go test 的常用 flag

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

更多详细,参考 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)
}
1
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)
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 跳过长任务

通过 -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)
}
1
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)
}
1
2
3
4
5
6
7
8
9
10

# 更加定制化的管理

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)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

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

在单元测试中,函数命名需要遵循 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
...
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

# 基准/性能测试 Benchmarks

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

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

# 最简单的例子

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

1
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
1
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
}
1
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
}
1
2
3
4
5
6
7
8
9
10
11

# 学习案例

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

# 参考资料


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK