测试
单元测试
单元测试(unit test)除用来测试逻辑算法是否复合预期外,还承担者监控代码质量的责任。
任何时候都可用简单的命令来验证全部功能,找出未完成任务(验收)和任何因修改而造成的错误。
它与性能测试、代码覆盖率等一起保障了代码总是在可控范围内,这远比形式化的人工检查要有用得多。
单元测试并非要取代人工代码审查(code review),实际上它也无法切入到代码实现层面。
但可通过测试结果为审查提供筛选依据,避免因烦琐导致代码审查沦为形式主义。
单元测试可自动化进行,能持之以恒。
但测试毕竟只是手段,而非目的,所以如何合理安排测试就需要开发人员因地制宜。
可将测试、版本管理工具,以及自动发布(nightly build)整合。
编写脚本将测试失败结果与代码提交日志相匹配,最终生成报告发往指定邮箱。
很多人认为单元测试代码不好写,不知道怎么测试。
如果非技术原因,那么需要考虑结构设计是否合理,因为可测试性也是代码质量的一个体现。
在我看来,写单元测试本身就是对即将要实现的算法做复核预演。
因为无论什么算法都需要输入条件,返回预期结果。
这些,加上平时写在 main 里面的临时代码,本就是一个完整的单元测试用例,无非换个地方存放而已。
testing
工具链和标准库自带单元测试框架,这让测试工作变得相对容易
- 测试代码须放在当前包以
_test.go
结尾的文件中
- 测试函数以 Test 为名称前缀
- 测试命令(go test)忽略以
_
或 .
开头的测试文件
- 正常编译操作(go build/install)会忽略测试文件
main_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13 | package main
import "testing"
func add(x, y int) int {
return x + y
}
func TestAdd(t *testing.T) {
if add(1, 2) != 3 {
t.FailNow()
}
}
|
输出:
| $ go test -v
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok github.com/YangzhenZhao/account-verify 0.463s
|
标准库 testing 提供了专用类型 T 来控制测试结果和行为
| 方法
说明
相关
Fail 失败: 继续执行当前测试函数
FailNow 失败: 立即终止执行当前测试函数 Failed
SkipNow 跳过: 停止执行当前测试花束 Skip,Skipf,Skipped
Log 输出错误信息。仅失败时或 -v 输出 Logf
Parallel 与有同样设置的测试函数并执行
Error Fail+Log Errorf
Fatal FailNow+Log Fatalf
|
使用 Parallel 可有效利用多核并行优势,缩短测试时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | package main
import (
"os"
"testing"
"time"
)
func TestA(t *testing.T) {
t.Parallel()
time.Sleep(time.Second * 2)
}
func TestB(t *testing.T) {
if os.Args[len(os.Args)-1] == "b" {
t.Parallel()
}
time.Sleep(time.Second * 2)
}
|
输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | $ go test -v
=== RUN TestA
=== PAUSE TestA
=== RUN TestB
--- PASS: TestB (2.00s)
=== CONT TestA
--- PASS: TestA (2.00s)
PASS
ok test 4.895s
$ go test -v -args "b"
=== RUN TestA
=== PAUSE TestA
=== RUN TestB
=== PAUSE TestB
=== CONT TestA
=== CONT TestB
--- PASS: TestB (2.00s)
--- PASS: TestA (2.00s)
PASS
ok github.com/YangzhenZhao/account-verify 2.724s
|
从测试总耗时可以看到并行执行的结果只有 2 秒
只有一个测试函数调用 Parallel 方法并没有效果,且 go test 执行参数 parallel 必须大于 1
常用测试参数
参数 |
说明 |
示例 |
-args |
命令行参数 |
|
-v |
输出详细信息 |
|
-parallel |
并发执行,默认只为 GOMAXPROCS |
-parallel 2 |
-run |
指定测试函数,正则表达式 |
-run "Add" |
-timeout |
全部测试累计时间超时将引发 panic,默认值为 10ms |
-timeout 1m30s |
-count |
重复测试次数,默认值为 1 |
|
对于测试是否应该和目标放在同一目录,一直有不同看法。
某些人认为应该另建一个专门的包用来存放单元测试,且只测试目标公开接口。
好处是,当目标内部发生变化时,无须同步维护测试代码。
每个人对于测试都有不同理解,就像覆盖率是否要做到 90% 以上,也是见仁见智
table driven
单元测试代码一样要写得简洁优雅,要做到这点并不容易。
好在多数时候,我们可用一种类似数据表的模式来批量输入条件并依次比对结果。
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 | package main
import (
"testing"
)
func add(x, y int) int {
return x + y
}
func TestAdd(t *testing.T) {
var tests = []struct {
x int
y int
expect int
}{
{1, 1, 2},
{2, 2, 4},
{3, 2, 5},
}
for _, tt := range tests {
actual := add(tt.x, tt.y)
if actual != tt.expect {
t.Errorf("add(%d, %d): expect %d, actual %d", tt.x, tt.y, tt.expect, actual)
}
}
}
|
这种方式将测试数据和测试逻辑分离,更便于维护。
另外,使用 Error 是为了让整个表全部完成测试,以便知道具体是哪组条件出现问题
test main
某些时候,须为测试用例提供初始化和清理操作,但 testing 并没有 setup/teardown 机制。
解决方法是自定义一个名为 TestMain 的函数,go test 会改为执行该函数,而不再是具体的测试用例。
| func TestMain(m *testing.M) {
// setup
code := m.Run() // 调用测试用例函数
// teardown
os.Exit(code) // 注意: os.Exit 不会执行 defer
}
|
M.Run 会调用具体的测试用例,但麻烦的是不能为每个测试文件写一个 TestMain。
| multiple definitions of TestMain
|
要实现用例组合套件(suite),须借助 MainStart 自行构建 M 对象。
通过与命令行参数相配合,即可实现不同测试组合。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | func TestMain(m *testing.M) {
match := func(pat, str string) (bool, error) { // 命令行参数 -run 提供的过滤条件
return true, nil // str:InternalTest.Name
}
tests := []testing.InternalTest{ // 用例列表,可排序
{"b", TestB},
{"a", TestA},
}
benchmarks := []testing.InternalBenchmark{}
examples := []testing.InternalExample{}
m = testing.MainStart(match, tests, benchmarks, examples)
os.Exit(m.run())
}
|
example
例代码最大的用途不是测试,而是导入到 GoDoc 等工具生成的帮助文档中。
它通过比对输出(stdout)结果和内部 output 注释是否一直来判断是否成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | package main
import (
"fmt"
)
func add(x, y int) int {
return x + y
}
func ExampleAdd() {
fmt.Println(add(1, 2))
fmt.Println(add(2, 2))
// Output:
// 3
// 4
}
|
输出:
| $ go test -v
=== RUN ExampleAdd
--- PASS: ExampleAdd (0.00s)
PASS
ok test 0.651s
|
如果没有 output 注释,该示例函数就不会被执行。
另外,不能使用内置函数 print/println,因为它们输出到 stderr
性能测试
性能测试函数以 Benchmark 为名称前缀,同样保存在 _test.go
文件里
1
2
3
4
5
6
7
8
9
10
11
12
13 | package main
import "testing"
func add(x, y int) int {
return x + y
}
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = add(1, 2)
}
}
|
| $ go test -bench .
goos: darwin
goarch: amd64
pkg: github.com/YangzhenZhao/account-verify
cpu: Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz
BenchmarkAdd-4 1000000000 0.3399 ns/op
PASS
ok test 1.183s
|
测试工具默认不会执行性能测试,须使用 bench 参数。
它通过逐步调整 B.N 值,反复执行测试函数,直到能获得准确的测量结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | package test_module
import "testing"
func add(x, y int) int {
return x + y
}
func BenchmarkAdd(b *testing.B) {
println("B.N = ", b.N)
for i := 0; i < b.N; i++ {
_ = add(1, 2)
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | $ go test -bench .
B.N = 1
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
BenchmarkAdd-16 B.N = 100
B.N = 10000
B.N = 1000000
B.N = 100000000
B.N = 1000000000
1000000000 0.2425 ns/op
PASS
ok _/Users/yangzhen.zhao/tmp/test_module 2.321s
|
如果希望仅执行性能测试,那么可以用 run=NONE
忽略所有单元测试用例
默认就以并发方式执行测试,但可用 cpu 参数设定多个并发限制来观察结果
| $ go test -bench . -cpu 1,2,4,8
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
BenchmarkAdd 1000000000 0.2578 ns/op
BenchmarkAdd-2 1000000000 0.2519 ns/op
BenchmarkAdd-4 1000000000 0.2546 ns/op
BenchmarkAdd-8 1000000000 0.2322 ns/op
PASS
ok _/Users/yangzhen.zhao/tmp/test_module 1.830s
|
某些耗时的目标,默认循环次数过少,取平均值不足以准确计量性能。
可用 benchtime 设定最小测试时间来增加循环次数,以便返回更准确的结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | package main
import (
"testing"
"time"
)
func sleep() {
time.Sleep(time.Second)
}
func BenchmarkSleep(b *testing.B) {
for i := 0; i < b.N; i++ {
sleep()
}
}
|
输出:
| $ go test -bench . -benchtime 5s
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
BenchmarkSleep-16 5 1001351177 ns/op
PASS
ok _/Users/yangzhen.zhao/tmp/test_module 11.836s
➜ test_module
|
timer
如果在测试函数中要执行一些额外操作,那么应该临时阻止计数器工作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 | package test_module
import (
"testing"
"time"
)
func add(x, y int) int {
return x + y
}
func BenchmarkAdd(b *testing.B) {
time.Sleep(time.Second)
b.ResetTimer() // 重置
for i := 0; i < b.N; i++ {
_ = add(1, 2)
if i == 1 {
b.StopTimer() // 暂停
time.Sleep(time.Second)
b.StartTimer() // 恢复
}
}
}
|
输出:
| $ go test -bench .
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
BenchmarkAdd-16 1000000000 0.2398 ns/op
PASS
ok _/Users/yangzhen.zhao/tmp/test_module 11.832s
|
memory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | package test_module
import (
"testing"
)
func heap() []byte {
return make([]byte, 1024*10)
}
func BenchmarkHeap(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = heap()
}
}
|
输出:
| # 禁用内联和优化,以便观察结果
$ go test -bench . -benchmem -gcflags "-N -l"
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
BenchmarkHeap-16 962241 1044 ns/op 10240 B/op 1 allocs/op
PASS
ok _/Users/yangzhen.zhao/tmp/test_module 1.810s
|
输出结果包括单次执行堆内存分配总量和次数
也可以将测试函数设置为总是输出内存分配信息,无论使用 benchmem 参数与否
| func BenchmarkHeap(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = heap()
}
}
|
代码覆盖率
test2333/utils.go
:
| package test2333
func Add(x, y int) int {
return x + y
}
|
test2333/my_test.go
:
```go
package test2333
import "testing"
func TestAdd(t *testing.T) {
if Add(1, 2) != 3 {
t.Fatal("xxx")
}
}
$ go test -cover
PASS
coverage: 100.0% of statements
ok _/Users/yangzhen.zhao/tmp/test2333 1.605s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | 为获取更详细的信息,可指定 cover-mode 和 coverprofile 参数
- set: 是否执行
- count: 执行次数
- atomic: 执行次数,支持并发模式
```bash
$ go test -cover -covermode count -coverprofile cover.out
PASS
coverage: 100.0% of statements
ok _/Users/yangzhen.zhao/tmp/test2333 2.013s
$ go tool cover -func=cover.out
/Users/yangzhen.zhao/tmp/test2333/utils.go:3: Add 100.0%
total: (statements) 100.0%
|
还可以在浏览器中查看包括具体的执行次数等信息
| $ go tool cover -html=cover.out
|
示例
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
36
37
38
39
40
41
42
43 | package main
import (
"fmt"
"testing"
)
func IntMin(a, b int) int {
if a < b {
return a
}
return b
}
func TestIntMinBasic(t *testing.T) {
ans := IntMin(2, -2)
if ans != -2 {
t.Errorf("IntMin(2, -2) = %d; want -2", ans)
}
}
func TestIntMinTableDriven(t *testing.T) {
var tests = []struct {
a, b int
want int
}{
{0, 1, 0},
{1, 0, 0},
{2, -2, -2},
{0, -1, -1},
{-1, 0, -1},
}
for _, tt := range tests {
testname := fmt.Sprintf("%d,%d", tt.a, tt.b)
t.Run(testname, func(t *testing.T) {
ans := IntMin(tt.a, tt.b)
if ans != tt.want {
t.Errorf("got %d, want %d", ans, tt.want)
}
})
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | $ go test -v
== RUN TestIntMinBasic
--- PASS: TestIntMinBasic (0.00s)
=== RUN TestIntMinTableDriven
=== RUN TestIntMinTableDriven/0,1
=== RUN TestIntMinTableDriven/1,0
=== RUN TestIntMinTableDriven/2,-2
=== RUN TestIntMinTableDriven/0,-1
=== RUN TestIntMinTableDriven/-1,0
--- PASS: TestIntMinTableDriven (0.00s)
--- PASS: TestIntMinTableDriven/0,1 (0.00s)
--- PASS: TestIntMinTableDriven/1,0 (0.00s)
--- PASS: TestIntMinTableDriven/2,-2 (0.00s)
--- PASS: TestIntMinTableDriven/0,-1 (0.00s)
--- PASS: TestIntMinTableDriven/-1,0 (0.00s)
PASS
ok examples/testing 0.023s
|