Go Test测试教程

go中测试既有类似有pytest中的功能测试,也有benchMark的基准测试,以及单元测试(Unit TestsUT).这里从单元测试UT引入本篇的话题,单元测试的重要性不言而喻,尤其在大型项目中跨团队合作时,无法mr合格的代码,很容易影响整个团队的交付进度和质量。或者会说直接debug,但是当你的代码是几千行的时候,这个时候debug似乎也比较累,那单元测试就能覆盖上述情况。

如何写好单元测试呢?

测试用例编写是基础。比如如何编写单个测试函数和单个测试方法,如何做基准测试,如何Mock数据等等,对于可测试的代码,高内聚,低耦合是软件工程的基本要求。同样对于测试而言,函数和方法写法不同,测试难度也是不一样的,参数少且类型单一,与其他函数耦合度较低这种函数更容易测试,其他的比如入参较多,且耦合多较高,是输入参数较多这种情况,结合正交法,等价类划分等设计方法,可以大大较少case设计的难度和复杂度,且能提升测试覆盖面。

这里概述一下go中支持的三种测试类型,分别是单元/功能测试性能(压力)测试覆盖率测试

接下来介绍使用Go的标准测试库 testing 进行单元测试

1.单元测试

go语言中推荐测试文件和源代码文件放置在一起,测试文件以_test.go结尾,这里可以和pytest进行类比,当前的packagecalculate.go文件,向测试calculate.go中有AddMul函数,应该新建一个calculate_test.go测试文件,所以习惯会将测试文件命名为功能文件_test.go的形式。
在这里插入图片描述

1.1 入门示例

编写go test测试函数时,如下所示,输入test,自动联想
在这里插入图片描述
单元测试文件可以有多个测试用例组成,每个测试用例的名称前缀必须是Test开头

func TestXxx( t \*testing.T ){  
    //......  
}

函数和单测的文件如下:

# calculate.go
package calculate

func Add(a,b int) int {
	return a+b
}

func Mult(a,b int) int {
	return a *b
}
# calculate_test.go
package calculate

import "testing"

func TestAdd(t *testing.T) {
	if ans := Add(2,3); ans != 5{
		t.Errorf("2+3 expected be 5,but %d got",ans)
	}

	if ans := Add(-10, -20); ans != -30 {
		t.Errorf("-10 + -20 expected be -30, but %d got", ans)
	}
}

在这里插入图片描述
在goland终端执行go test

xxx@yyy calculate % ls
calculate.go            calculate_test.go       go.mod
xxx@yyy calculate % go test -v
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok      calculate       0.137s

go test -v-v 参数会显示每个用例的测试结果。如果继续编写mul的测试函数.此时calculate_test.go中两个测试函数,如果想仅仅指定其中一个测试用例,比如TestMul,可以使用-run参数

 go test -run TestMul -v
=== RUN   TestMul
--- PASS: TestMul (0.00s)
PASS
ok      calculate       0.531s

go test <module name>/<package name> 用来运行某个 package 内的所有测试用例。

运行当前 package 内的用例:go test calcuatego test .
运行子 package 内的用例: go test calcuate/<package name>go test ./<package name>
如果想递归测试当前目录下的所有的 package:go test ./...go test calcuate/...

该参数还支持通配符*,和部分正则表达式,如^$

1.2 子测试

这里就不得不提到子测试,即所谓的Subtests,该功能是go语言内置的支持的功能,可以在一个测试用例中,根据测试场景使用t.Run创建不同的子测试用例:

func TestMul(t *testing.T) {
	//if ans := Mul(2,3); ans != 6{
	//	t.Errorf("2*3 expected be 6,but %d got",ans)
	//}
	t.Run("pos", func(t *testing.T) {
		if Mul(2, 3) != 6{
			t.Fatalf("2*3 expected be 6, but %d got", Mul(2, 3))
		}
	})
	t.Run("neg", func(t *testing.T) {
		if Mul(2, -3) != -6{
			t.Fatalf("2*-3 expected be 6, but %d got", Mul(2, -3))
		}
	})
}

除了使用命令之后,可以使用golang中的如下符号
在这里插入图片描述
输出如下所示

/usr/local/Cellar/go/1.19.6/libexec/bin/go tool test2json -t /private/var/folders/cc/wn7xg4yx22d_qp96zw37rrl00000gp/T/___TestMul_in_calculate.test -test.v -test.paniconexit0 -test.run ^\QTestMul\E$
=== RUN   TestMul
=== RUN   TestMul/pos
=== RUN   TestMul/neg
--- PASS: TestMul (0.00s)
    --- PASS: TestMul/pos (0.00s)
    --- PASS: TestMul/neg (0.00s)
PASS

Process finished with the exit code 0

📢:之前的例子失败是使用的t.Error/t.Errorf,这里使用的是t.Fatal/Fatalf,区别在于前者遇到错误不会停止,还会继续执行其他的测试用例,后者遇到❎就会停止。
执行运行其中某个子测试

 calculate % go test -run TestMul/pos -v
=== RUN   TestMul
=== RUN   TestMul/pos
--- PASS: TestMul (0.00s)
    --- PASS: TestMul/pos (0.00s)
PASS
ok      calculate       1.002s

种类的Run()第一个参数是不是类似Pytest中的mark.tag标签,但是上面的写法发现冗余别较多,推荐使用如下的方法

func TestMul(t *testing.T) {
	//if ans := Mul(2,3); ans != 6{
	//	t.Errorf("2*3 expected be 6,but %d got",ans)
	//}
	//t.Run("pos", func(t *testing.T) {
	//	if Mul(2, 3) != 6{
	//		t.Fatalf("2*3 expected be 6, but %d got", Mul(2, 3))
	//	}
	//})
	//t.Run("neg", func(t *testing.T) {
	//	if Mul(2, -3) != -6{
	//		t.Fatalf("2*-3 expected be 6, but %d got", Mul(2, -3))
	//	}
	//})

	cases := []struct {
		Name           string
		A, B, Expected int
	}{
		{"pos", 2, 3, 6},
		{"neg", 2, -3, -6},
		{"zero", 0, 2, 0},
	}

	for _, c := range cases {
		t.Run(c.Name, func(t *testing.T) {
			if ans := Mul(c.A, c.B); ans != c.Expected {
				t.Fatalf("%d * %d expected %d, but %d got", c.A, c.B, c.Expected, ans)
			}
		})
	}
}

输出如下所示:

calculate % go test -run TestMul -v
=== RUN   TestMul
=== RUN   TestMul/pos
=== RUN   TestMul/neg
=== RUN   TestMul/zero
--- PASS: TestMul (0.00s)
    --- PASS: TestMul/pos (0.00s)
    --- PASS: TestMul/neg (0.00s)
    --- PASS: TestMul/zero (0.00s)
PASS
ok      calculate       0.343s

上面的用法和pytest中的@pytest.mark.parametrize(‘status’, [‘Pending’, ‘Running’, ‘Success’, ‘Failed’, ‘Timeout’])进行类比,所有用例的测试数据组织在切片cases中,借助于创建子测试,当然目前觉得pytest这样更方便些,go这样写的好处有:

  1. 新增用例非常简单,只需给 cases 新增一条测试数据即可。
  2. 测试代码可读性好,直观地能够看到每个子测试的参数和期待的返回值。
  3. 用例失败时,报错信息的格式比较统一,测试报告易于阅读。

如果数据量较大,或是一些二进制数据,推荐使用相对路径从文件中读取

所以编写测试用例可以抽象总结为以下几点:

  • 测试用例文件不会参与正常源码的编译,不会被包含到可执行文件中;
  • 测试用例的文件名必须以_test.go结尾;
  • 需要使用 import 导入 testing 包;
  • 测试函数的名称要以Test或Benchmark开头,后面可以跟任意字母组成的字符串,但第一个字母必须大写,例如 TestAbc(),一个测试用
  • 文件中可以包含多个测试函数;
  • 单元测试则以(t *testing.T)作为参数,性能测试以(t *testing.B)做为参数;
  • 测试用例文件使用go test命令来执行,源码中不需要 main() 函数作为入口,所有以_test.go结尾的源码文件内以Test开头的函数都会自动执行。

1.3 帮助函数helpers

我们知道在Pytest中公共的东西可以抽象出来放置在contest.py中,并设置使用级别,如session,function等,对于go中的testing,一些重复的逻辑可以抽出来作为公共的帮助函数helpers,这样的好处无需赘言,增加了测试代码的可维护性和可读性,且使得测试用例的逻辑更加紧凑和清晰,接着上面的示例

# calculate_test.go
package calculate

import "testing"

import "testing"

type calcCase struct {
	Name           string
	A, B, Expected int
}

func CreateMulTestCase(t *testing.T, c *calcCase, ) {
	//	t.helpers
	//	if ans := Mul(c.A, c.B); ans != c.Expected {
	//		t.Fatalf("%d * %d expected %d, but %d got", c.A, c.B, c.Expected, ans)
	//	}

	t.Helper()
	t.Run(c.Name, func(t *testing.T) {
		if ans := Mul(c.A, c.B); ans != c.Expected {
			t.Fatalf("%d * %d expected %d, but %d got", c.A, c.B, c.Expected, ans)
		}
	})
}

func TestMul(t *testing.T) {
	//if ans := Mul(2,3); ans != 6{
	//	t.Errorf("2*3 expected be 6,but %d got",ans)
	//}
	//t.Run("pos", func(t *testing.T) {
	//	if Mul(2, 3) != 6{
	//		t.Fatalf("2*3 expected be 6, but %d got", Mul(2, 3))
	//	}
	//})
	//t.Run("neg", func(t *testing.T) {
	//	if Mul(2, -3) != -6{
	//		t.Fatalf("2*-3 expected be 6, but %d got", Mul(2, -3))
	//	}
	//})

	//cases := []struct {
	//	Name           string
	//	A, B, Expected int
	//}{
	//	{"pos", 2, 3, 6},
	//	{"neg", 2, -3, -6},
	//	{"zero", 0, 2, -1},
	//}
	//
	//for _, c := range cases {
	//	t.Run(c.Name, func(t *testing.T) {
	//		if ans := Mul(c.A, c.B); ans != c.Expected {
	//			t.Fatalf("%d * %d expected %d, but %d got", c.A, c.B, c.Expected, ans)
	//		}
	//	})
	//}

	//CreateMulTestCase(t,&calcCase{2,3,6})
	//CreateMulTestCase(t,&calcCase{2,-3,-6})
	//CreateMulTestCase(t,&calcCase{0,2,1})

	CreateMulTestCase(t, &calcCase{"pos", 2, 3, 6})
	CreateMulTestCase(t, &calcCase{"neg", 2, -3, -6})
	CreateMulTestCase(t, &calcCase{"zero", 0, 2, 1})
}

# 这里给出了所以的代码,方便对比查阅,感觉其中演进变化之处

执行结果如下

calculate % go test -run TestMul -v                         
=== RUN   TestMul
    calculate_test.go:12: 0 * 2 expected 1, but 0 got
--- FAIL: TestMul (0.00s)
FAIL
exit status 1
FAIL    calculate       0.779s

发现有一个失败了,检查一下发现错误❎定位是第12行,我们回溯下
在这里插入图片描述
但是这里有三个case都调用了,具体是哪个case有问题还需要一个个排查,这样也太麻烦了吧。因此go 1.9版本中引入了t.Helper(),用于标注该函数是帮助函数,报错时将输出帮助函数调用者的信息,而不是帮助函数内部的信息。检查本机的go版本

calculate % go version
go version go1.19.6 darwin/amd64

然后修改CreateMulTestCase,调用t.Helper()

func CreateMulTestCase(c *calcCase, t *testing.T) {
//	t.helpers
//	if ans := Mul(c.A, c.B); ans != c.Expected {
//		t.Fatalf("%d * %d expected %d, but %d got", c.A, c.B, c.Expected, ans)
//	}

	t.Helper()
	t.Run(c.Name, func(t *testing.T) {
		if ans := Mul(c.A, c.B); ans != c.Expected {
			t.Fatalf("%d * %d expected %d, but %d got", c.A, c.B, c.Expected, ans)
		}
	})
}

输出如下所示:

calculate % go test -run TestMul -v
=== RUN   TestMul
=== RUN   TestMul/pos
=== RUN   TestMul/neg
=== RUN   TestMul/zero
    calculate_test.go:61: 0 * 2 expected -1, but 0 got
--- FAIL: TestMul (0.00s)
    --- PASS: TestMul/pos (0.00s)
    --- PASS: TestMul/neg (0.00s)
    --- FAIL: TestMul/zero (0.00s)
FAIL
exit status 1
FAIL    calculate       0.397s

对于使用t.Helper(),有两点要注意:

  1. 不要返回错误, 帮助函数内部直接使用 t.Error 或 t.Fatal 即可,在用例主逻辑中不会因为太多的错误处理代码,影响可读性。
  2. 调用 t.Helper() 让报错信息更准确,有助于定位。

1.4 setup和teardown

一般我们编写自动化测试用例时,非业务检查逻辑会放置前者准备中诸如数据准备,抽象出一部分公共逻辑写在 setup 和 teardown 函数中。例如执行前需要实例化待测试的对象,如果这个对象比较复杂,很适合将这一部分逻辑提取出来;执行后,可能会做一些资源回收类的工作,例如关闭网络连接,释放文件等。标准库 testing 提供了这样的机制。


2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func setup() {
	fmt.Println("Before all tests")
}

func teardown() {
	fmt.Println("After all tests")
}

func Test1(t *testing.T) {
	fmt.Println("I'm test1")
}

func Test2(t *testing.T) {
	fmt.Println("I'm test2")
}

func TestMain(m *testing.M) {
	setup()
	code := m.Run()
	teardown()
	os.Exit(code)
}
  • 在这个测试文件中,包含有2个测试用例,Test1 和 Test2。
  • 如果测试文件中包含函数 TestMain,那么生成的测试将调用 TestMain(m),而不是直接运行测试。
  • 调用 m.Run() 触发所有测试用例的执行,并使用 os.Exit() 处理返回的状态码,如果不为0,说明有用例失败。
  • 因此可以在调用 m.Run() 前后做一些额外的准备(setup)和回收(teardown)工作。

执行 go test,将会输出

$ go test
Before all tests
I'm test1
I'm test2
PASS
After all tests
ok      example 0.006s

2.性能测试

对于性能测试,Go语言标准库内置的testing测试框架提供了基准测试benchmark的能力,可以很容易的对一段代码进行性能测试。性能测试受环境的影响很大,为了保证测试的可重复性,在进行性能测试时,尽可能地保持测试环境的稳定。

  • 机器处于闲置状态,测试时不要执行其他任务,也不要和其他人共享硬件资源。
  • 机器是否关闭了节能模式,一般笔记本会默认打开这个模式,测试时关闭。
  • 避免使用虚拟机和云主机进行测试,一般情况下,为了尽可能地提高资源的利用率,虚拟机和云主机 CPU 和内存一般会超分配,超分机器的性能表现会非常地不稳定。

2.1 入门示例

还是从一个简单的示例开始介绍,使用斐波那契数列,接着上面的calculate.go中新增Fib
在这里插入图片描述

func Fib(n int) int {
	if n == 0 || n == 1 {
		return n
	}
	return Fib(n-2) + Fib(n-1)
}

calculate_test.go中实现一个benchmark用例,和单元测试一样,输入test会快捷联想出来

func BenchmarkFib(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Fib(30)
	}
}
  • benchmark 和普通的单元测试用例一样,都位于 _test.go 文件中。
  • 函数名以 Benchmark 开头,参数是 b *testing.B。和普通的单元测试用例很像,单元测试函数名以 Test 开头,参数是 t *testing.T

运行示例

go test <module name>/<package name> 用来运行某个 package 内的所有测试用例。

运行当前 package 内的用例:go test calculatego test .
运行子 package 内的用例: go test calculate/<package name>go test ./<package name>
如果想递归测试当前目录下的所有的 package:go test ./...go test calculate/...
go test 命令默认不运行 benchmark 用例的,如果我们想运行 benchmark 用例,则需要加上 -bench 参数。

calculate % go test -bench .
goos: darwin
goarch: amd64
pkg: calculate
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkFib-12              327           3546075 ns/op
PASS
ok      calculate       1.887s

-bench 参数支持传入一个正则表达式,匹配到的用例才会得到执行,例如,只运行以 Fib 结尾的 benchmark

calculate % go test -bench="Fib$" .
goos: darwin
goarch: amd64
pkg: calculate
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkFib-12              325           3589261 ns/op
PASS
ok      calculate       2.174s

2.3 benchmark是如何工作的

benchmark 用例的参数 b *testing.B,有个属性 b.N 表示这个用例需要运行的次数。b.N 对于每个用例都是不一样的。

那这个值是如何决定的呢?b.N 从 1 开始,如果该用例能够在 1s 内完成,b.N 的值便会增加,再次执行。b.N 的值大概以 1, 2, 3, 5, 10, 20, 30, 50, 100 这样的序列递增,越到后面,增加得越快。我们仔细观察上述例子的输出:

BenchmarkFib-12               202           5980669 ns/op

BenchmarkFib-12 中的 -12 即 GOMAXPROCS,默认等于 CPU 核数。可以通过 -cpu 参数改变 GOMAXPROCS-cpu 支持传入一个列表作为参数,例如

calculate % go test -bench="Fib$" -cpu=2,4 .
goos: darwin
goarch: amd64
pkg: calculate
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkFib-2               332           3557771 ns/op
BenchmarkFib-4               343           3512955 ns/op
PASS
ok      calculate       3.460s

在这个例子中,改变 -cpu 的核数对结果几乎没有影响,因为这个 Fib 的调用是串行的。

3323557771 ns/op 表示用例执行了 343 次,每次花费约 0.006s。总耗时比 1s 略多

3.3 提升准确度

对于性能测试来说,提升测试准确度是一个重要手段就是增加测试的次数,即所谓的常稳测试,可以使用-benchtime-count,其中-benchtime默认为1s,可以设置指定-benchtime=5s`,如:

 calculate % go test -bench="Fib$" -benchtime=5s .
goos: darwin
goarch: amd64
pkg: calculate
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkFib-12             1712           3466178 ns/op
PASS
ok      calculate       6.855s

实际执行的时间是 6.8s,比 benchtime 的 5s 要长,测试用例编译、执行、销毁等是需要时间的。 `-benchtime` 设置为 5s,用例执行次数也变成了原来的 5倍,每次函数调用时间仍为 0.6s,几乎没有变化。

`-benchtime` 的值除了是时间外,还可以是具体的次数。例如,执行 30 次可以用 `-benchtime=50x`

 calculate % go test -bench="Fib$" -benchtime=30x .
goos: darwin
goarch: amd64
pkg: calculate
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkFib-12               30           3683029 ns/op
PASS
ok      calculate       1.113s

Fib调用了30次,花费1.1s。接着使用-count设置执行benchmark的论数,有点类似pytest中的repeat,例如进行5轮测试

 calculate % go test -bench="Fib$"  -benchtime=10s -count=5 .
goos: darwin
goarch: amd64
pkg: calculate
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkFib-12             3358           3493746 ns/op
BenchmarkFib-12             3330           3466845 ns/op
BenchmarkFib-12             3472           3440367 ns/op
BenchmarkFib-12             3442           3431086 ns/op
BenchmarkFib-12             3379           3429828 ns/op
PASS
ok      calculate       61.065s

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值