为什么需要测试?
- 完善的测试体系,能够提高开发的效率,当项目足够复杂的时候,想要保证尽可能的减少 bug。
- 有两种有效的方式分别是代码审核和代码测试,Go语言中提供了 testing 包来实现测试功能。
- 我们编写完一个模块时,应该立马编写测试用例,养成良好习惯,不要偷懒。
介绍
- Go语言自带了 testing 测试包,可以进行自动化测试,且提供了三种测试方式:
- 单元(功能)测试
- 性能(基准)测试
- 覆盖率测试
- 编写测试用例有以下几点需要注意:
- 测试用例文件不会参与正常源码的编译,不会被包含到可执行文件中。
- 测试用例文件使用
go test
命令来执行,源码中不需要 main() 函数作为入口。 - 测试用例的文件名必须以
_test.go
结尾,建议命名为模块名_test.go
。需要使用 import 导入 testing 包。一个测试用例文件中可以包含多个测试函数。所有以_test.go
结尾的源码文件内以Test
开头的函数都会自动执行。
测试
1. 单元(功能)测试
- 单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java 里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。
- 单元测试函数以 Test 开头,后面可以跟任意字母组成的字符串,但第一个字母必须大写。
例如func TestAbc(t *testing.T)
。 - 参数如下: | -run regexp | 只运行 regexp 匹配的函数,例如 -run=Array 那么就执行包含有 Array 开头的函数; | | —- | —- | | -v | 显示测试的详细命令 | | -cover | 开启测试覆盖率 |
示例
demo_test.go:
package demo_test
import "testing"
func TestA(t *testing.T) {
t.Log("A")
}
func TestAK(t *testing.T) {
t.Log("AK")
}
go test
默认执行文件内的所有测试用例:
$ go test -v
=== RUN TestA
--- PASS: TestA (0.00s)
demo_test.go:5: A
=== RUN TestAK
--- PASS: TestAK (0.00s)
demo_test.go:8: AK
PASS
ok unitTesting/demo_test 0.004s
使用-run
参数选择需要的测试用例单独执行(支持正则)
$ go test -v -run Ab
=== RUN TestAbc
--- PASS: TestAbc (0.00s)
s_test.go:10: abc
=== RUN TestAb
--- PASS: TestAb (0.00s)
s_test.go:14: ab
PASS
ok unitTesting/pp 0.004s
TestA 和 TestAK 的测试用例都被执行,原因是-run
跟随的测试用例的名称支持正则表达式,使用-run TestA$
即可只执行 TestA 测试用例。
覆盖率测试
覆盖率测试能知道测试程序总共覆盖了多少业务代码(也就是 demo_test.go 中测试了多少 demo.go 中的代码),可以的话最好是覆盖100%。
demo_test.go :
package demo
import "testing"
func TestHello(t *testing.T) {
t.Log("hello")
}
执行测试命令,运行结果如下所示:
$ pp go test -v -cover
=== RUN TestHello
--- PASS: TestHello (0.00s)
demo_test.go:5: hello
PASS
coverage: 0.0% of statements
ok unitTesting/pp 0.004s
2.性能(基准)测试
- 基准测试可以测试一段程序的运行性能及耗费 CPU 的程度。Go语言中提供了基准测试框架,使用方法类似于单元测试,使用者无须准备高精度的计时器和各种分析工具,基准测试本身即可以打印出非常标准的测试报告。
- 性能测试以为
Benchmark
开头,后面可以跟任意字母组成的字符串,但第一个字母必须大写。
例如func TestAbc(t *testing.B)
。 - 参数如下: | -bench regexp | 执行相应的 benchmarks,例如 -bench=.; | | —- | —- | | -v | 显示测试的详细命令 |
原理
基准测试框架对一个测试用例的默认测试时间是 1 秒。开始测试时,当以 Benchmark 开头的基准测试用例函数返回时还不到 1 秒,那么 testing.B 中的 N 值将按 1、2、5、10、20、50……递增,同时以递增后的值重新调用基准测试用例函数。
示例
demo_test.go:
第 6 行中的 b.N 由基准测试框架提供。测试代码需要保证函数可重入性及无状态,也就是说,测试代码不使用全局变量等带有记忆性质的数据结构。避免多次运行同一段代码时的环境不一致,不能假设 N 值范围。
package demo
import "testing"
func Benchmark_Add(t *testing.B) {
var n int
for i := 0; i < b.N; i++ {
n++
}
}
测试:
$ go test -v -bench=. benchmark_test.go
goos: linux
goarch: amd64
Benchmark_Add-4 20000000 0.33 ns/op
PASS
ok command-line-arguments 0.700s
- 第 1 行的
-bench=.
表示运行 benchmark_test.go 文件里的所有基准测试,和单元测试中的-run
类似。 - 第 4 行中显示基准测试名称,2000000000 表示测试的次数,也就是 testing.B 结构中提供给程序使用的 N。“0.33 ns/op”表示每一个操作耗费多少时间(纳秒)。
注意:Windows 下使用 go test 命令行时,
-bench=.
应写为-bench="."
。
自定义测试时间
通过-benchtime
参数可以自定义测试时间,例如:
$ go test -v -bench=. -benchtime=5s benchmark_test.go
goos: linux
goarch: amd64
Benchmark_Add-4 10000000000 0.33 ns/op
PASS
ok command-line-arguments 3.380s
测试内存
可以对一段代码可能存在的内存分配进行统计,下面是一段使用字符串格式化的函数,内部会进行一些分配操作。
func Benchmark_Alloc(b *testing.B) {
for i := 0; i < b.N; i++ {
fmt.Sprintf("%d", i)
}
}
在命令行中添加-benchmem
参数以显示内存分配情况,参见下面的指令:
$ go test -v -bench=Alloc -benchmem benchmark_test.go
goos: linux
goarch: amd64
Benchmark_Alloc-4 20000000 109 ns/op 16 B/op 2 allocs/op
PASS
ok command-line-arguments 2.311s
- 第 1 行的代码中
-bench
后添加了 Alloc,指定只测试 Benchmark_Alloc() 函数。 - 第 4 行代码的“16 B/op”表示每一次调用需要分配 16 个字节,“2 allocs/op”表示每一次调用有两次分配。
控制计时器
- 有些测试需要一定的启动和初始化时间,如果从 Benchmark() 函数开始计时会很大程度上影响测试结果的精准性。testing.B 提供了一系列的方法可以方便地控制计时器,从而让计时器只在需要的区间进行测试。
- 计数器内部不仅包含耗时数据,还包括内存分配的数据。
从 Benchmark() 函数开始,Timer 就开始计数。StopTimer() 可以停止这个计数过程,做一些耗时的操作,通过 StartTimer() 重新开始计时。ResetTimer() 可以重置计数器的数据。func Benchmark_Add_TimerControl(b *testing.B) {
// 停止计时器
b.StopTimer()
// 重置计时器
b.ResetTimer()
// 开始计时器
b.StartTimer()
var n int
for i := 0; i < b.N; i++ {
n++
}
}
公共方法
单元测试日志
多个测试用例可能并发执行,使用提供的日志输出可以保证日志跟随这个测试上下文一起打印输出。
方 法 | 备 注 |
---|---|
Log | 打印日志,同时结束测试 |
Logf | 格式化打印日志,同时结束测试 |
Error | 打印错误日志,同时结束测试 |
Errorf | 格式化打印错误日志,同时结束测试 |
Fatal | 打印致命日志,同时结束测试 |
Fatalf | 格式化打印致命日志,同时结束测试 |
标记测试结果
当需要终止当前测试用例时,可以使用 FailNow
func TestFailNow(t *testing.T) {
t.FailNow()
}
当需要标记错误不终止测试的方法,可以使用 Fail()
func TestFail(t *testing.T) {
t.Log("before fail")
t.Fail()
t.Log("after fail")
}
测试如下:
$ go test
=== RUN TestFail
before fail
after fail
--- FAIL: TestFailNow (0.00s)
FAIL
exit status 1
FAIL command-line-arguments 0.002s