go中测试既有类似有pytest
中的功能测试,也有benchMark
的基准测试,以及单元测试(Unit Tests
,UT
).这里从单元测试UT引入本篇的话题,单元测试的重要性不言而喻,尤其在大型项目中跨团队合作时,无法mr合格的代码,很容易影响整个团队的交付进度和质量。或者会说直接debug,但是当你的代码是几千行的时候,这个时候debug似乎也比较累,那单元测试就能覆盖上述情况。
如何写好单元测试呢?
测试用例编写是基础。比如如何编写单个测试函数和单个测试方法,如何做基准测试,如何Mock
数据等等,对于可测试的代码,高内聚,低耦合是软件工程的基本要求。同样对于测试而言,函数和方法写法不同,测试难度也是不一样的,参数少且类型单一,与其他函数耦合度较低这种函数更容易测试,其他的比如入参较多,且耦合多较高,是输入参数较多这种情况,结合正交法,等价类划分等设计方法,可以大大较少case设计的难度和复杂度,且能提升测试覆盖面。
这里概述一下go中支持的三种测试类型,分别是单元/功能测试
,性能(压力)测试
,覆盖率测试
接下来介绍使用Go的标准测试库 testing
进行单元测试
1.单元测试
go语言中推荐测试文件和源代码文件放置在一起,测试文件以_test.go
结尾,这里可以和pytest进行类比,当前的package
有calculate.go
文件,向测试calculate.go
中有Add
和Mul
函数,应该新建一个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 calcuate
或 go 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这样写的好处有:
- 新增用例非常简单,只需给 cases 新增一条测试数据即可。
- 测试代码可读性好,直观地能够看到每个子测试的参数和期待的返回值。
- 用例失败时,报错信息的格式比较统一,测试报告易于阅读。
如果数据量较大,或是一些二进制数据,推荐使用相对路径从文件中读取
所以编写测试用例可以抽象总结为以下几点:
- 测试用例文件不会参与正常源码的编译,不会被包含到可执行文件中;
- 测试用例的文件名必须以
_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(),有两点要注意:
- 不要返回错误, 帮助函数内部直接使用 t.Error 或 t.Fatal 即可,在用例主逻辑中不会因为太多的错误处理代码,影响可读性。
- 调用 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 calculate
或 go 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
的调用是串行的。
332
和 3557771 ns/op
表示用例执行了 343
次,每次花费约 0.006s。总耗时比 1s 略多
3.3 提升准确度
对于性能测试来说,提升测试准确度是一个重要手段就是增加测试的次数,即所谓的常稳测试,可以使用-benchtime
和-count
,其中-benchtime
默认为1s,可以设置指定-benchtime
=5s`,如:文章来源:https://www.toymoban.com/news/detail-551200.html
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轮测试文章来源地址https://www.toymoban.com/news/detail-551200.html
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
到了这里,关于Go Test测试教程的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!