Go Test

本文最后更新于:2024年3月18日 凌晨

Go Test

go test 工具

  • Go 语言中的测试依赖 go test 命令,编写测试代码和编写普通的 Go 代码过程是类似的,并不需要学习新的语法,规则或工具。
  • go test 命令是一个按照一定约定和组织的测试代码的驱动程序,在包目录内,所有以 _test.go 为后缀名的源代码文件都是 go test 测试的一部分,不会被 go build 编译到最终的可执行文件中。
  • *_test.go 文件中有三种类型的函数,单元测试函数,基准测试函数和示例函数。
类型 格式 作用
测试函数 函数名前缀为 Test 测试程序的一些逻辑行为是否正确
基准函数 函数名前缀为 Benchmark 测试函数的性能
示例函数 函数名前缀为 Example 为文档提供示例文档
  • go test 命令会遍历所有的 *_test.go 文件中符合上述命名规则的函数,然后生成一个临时的 main 包用于调用相应的测试函数,然后构建并运行,报告测试结果,最后清理测试中生成的临时文件。

测试函数

测试函数的格式

  • 每个测试函数必须导入 testing 包,测试函数的基本格式(签名)如下:
1
2
3
func TestName(t *testing.T){
// ...
}
  • 测试函数的名字必须以 Test 开头,可选的后缀名必须以大写字母开头,举几个例子:
1
2
3
func TestAdd(t *testing.T){ ... }
func TestSum(t *testing.T){ ... }
func TestLog(t *testing.T){ ... }
  • 其中参数 t 用于报告测试失败和附加的日志信息, testing.T 的拥有的方法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (t *T) Parallel()
func (t *T) Run(name string, f func(t *T)) bool
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool

测试函数示例

  • 一个软件程序也是由很多单元组件构成的,单元组件可以是函数,结构体,方法和最终用户可能依赖的任意东西,总之我们需要确保这些组件是能够正常运行的,单元测试是一些利用各种方法测试单元组件的程序,它会将结果与预期输出进行比较。

  • 在当前目录下,我们创建一个 split_test.go 的测试文件,并定义一个测试函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// split/split_test.go

package split

import (
"reflect"
"testing"
)

func TestSplit(t *testing.T) { // 测试函数名必须以Test开头,必须接收一个*testing.T类型参数。
got := Split("a:b:c", ":") // 程序输出的结果。
want := []string{"a", "b", "c"} // 期望的结果。
if !reflect.DeepEqual(want, got) { // 因为slice不能比较直接,借助反射包中的方法比较。
t.Errorf("expected:%v, got:%v", want, got) // 测试失败输出错误提示。
}
}
  • split 包路径下,执行 go test 命令,可以看到输出结果如下:
1
2
3
$ go test
PASS
ok github.com/Test/test_demo/split 0.005s
  • -v:输出详细信息。

  • -run:参数,它对应一个正则表达式,只有函数名匹配上的测试函数才会被 go test 命令执行。

  • 再次运行 go test 命令,输出结果如下:

1
2
3
4
5
6
$ go test
--- FAIL: TestMultiSplit (0.00s)
split_test.go:20: expected:[a d], got:[a cd]
FAIL
exit status 1
FAIL github.com/Test/test_demo/split 0.006s
  • 这一次,我们的测试失败了,我们可以为 go test 命令添加 -v 参数,查看测试函数名称和运行时间:
1
2
3
4
5
6
7
8
9
$ go test -v
=== RUN TestSplit
--- PASS: TestSplit (0.00s)
=== RUN TestMoreSplit
--- FAIL: TestMoreSplit (0.00s)
split_test.go:21: expected:[a d], got:[a cd]
FAIL
exit status 1
FAIL github.com/Test/test_demo/split 0.005s
1
2
3
4
5
6
7
$ go test -v -run="More"
=== RUN TestMoreSplit
--- FAIL: TestMoreSplit (0.00s)
split_test.go:21: expected:[a d], got:[a cd]
FAIL
exit status 1
FAIL github.com/Test/test_demo/split 0.006s

测试组

  • 我们现在还想要测试一下 split 函数对中文字符串的支持,这个时候我们可以再编写一个 TestChineseSplit 测试函数,但是我们也可以使用如下更友好的一种方式来添加更多的测试用例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func TestSplit(t *testing.T) {
// 定义一个测试用例类型。
type test struct {
input string
sep string
want []string
}
// 定义一个存储测试用例的切片。
tests := []test{
{input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
{input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
{input: "abcd", sep: "bc", want: []string{"a", "d"}},
{input: "沙河有沙又有河", sep: "沙", want: []string{"河有", "又有河"}},
}
// 遍历切片,逐一执行测试用例。
for _, tc := range tests {
got := Split(tc.input, tc.sep)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("expected:#v, got:#v", tc.want, got)
}
}
}
  • 这种情况下十分推荐使用 %#v 的格式化方式。
1
2
3
4
5
6
7
$ go test -v
=== RUN TestSplit
--- FAIL: TestSplit (0.00s)
split_test.go:42: expected:[]string{"河有", "又有河"}, got:[]string{"", "河有", "又有河"}
FAIL
exit status 1
FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s

子测试

  • Go 1.7+中新增了子测试,我们可以按照如下方式使用 t.Run 执行子测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func TestSplit(t *testing.T) {
type test struct { // 定义test结构体。
input string
sep string
want []string
}
tests := map[string]test{ // 测试用例使用map存储。
"simple": {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
"wrong sep": {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
"more sep": {input: "abcd", sep: "bc", want: []string{"a", "d"}},
"leading sep": {input: "沙河有沙又有河", sep: "沙", want: []string{"河有", "又有河"}},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) { // 使用t.Run()执行子测试。
got := Split(tc.input, tc.sep)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("expected:%#v, got:%#v", tc.want, got)
}
})
}
}
  • 此时我们再执行 go test 命令就能够看到更清晰的输出内容了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ go test -v
=== RUN TestSplit
=== RUN TestSplit/leading_sep
=== RUN TestSplit/simple
=== RUN TestSplit/wrong_sep
=== RUN TestSplit/more_sep
--- FAIL: TestSplit (0.00s)
--- FAIL: TestSplit/leading_sep (0.00s)
split_test.go:83: expected:[]string{"河有", "又有河"}, got:[]string{"", "河有", "又有河"}
--- PASS: TestSplit/simple (0.00s)
--- PASS: TestSplit/wrong_sep (0.00s)
--- PASS: TestSplit/more_sep (0.00s)
FAIL
exit status 1
FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s
  • 我们都知道可以通过 -run=RegExp 来指定运行的测试用例,还可以通过 / 来指定要运行的子测试用例,例如: go test -v -run=Split/simple 只会运行 simple 对应的子测试用例。

测试覆盖率

  • 测试覆盖率是你的代码被测试套件覆盖的百分比,通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。
  • Go 提供内置功能来检查你的代码覆盖率,我们可以使用 go test -cover 来查看测试覆盖率,例如:
1
2
3
4
$ go test -cover
PASS
coverage: 100.0% of statements
ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.005s
  • 从上面的结果可以看到我们的测试用例覆盖了 100%的代码。
  • Go 还提供了一个额外的 -coverprofile 参数,用来将覆盖率相关的记录信息输出到一个文件,例如:
1
2
3
4
$ go test -cover -coverprofile=c.out
PASS
coverage: 100.0% of statements
ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.005s
  • 上面的命令会将覆盖率相关的信息输出到当前文件夹下面的 c.out 文件中,然后我们执行 go tool cover -html=c.out,使用 cover 工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成一个 HTML报告。
Go test cover
  • 上图中每个用绿色标记的语句块表示被覆盖了,而红色的表示没有被覆盖。

基准测试

基准测试函数格式

  • 基准测试就是在一定的工作负载之下检测程序性能的一种方法,基准测试的基本格式如下:
1
2
3
func BenchmarkName (b *testing. B){
// ...
}
  • 基准测试以Benchmark为前缀,需要一个*testing. B类型的参数 b,基准测试必须要执行b.N次,这样的测试才有对照性,b.N的值是系统根据实际情况去调整的,从而保证测试的稳定性,testing. B拥有的方法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func (c *B) Error (args ... interface{})
func (c *B) Errorf (format string, args ... interface{})
func (c *B) Fail ()
func (c *B) FailNow ()
func (c *B) Failed () bool
func (c *B) Fatal (args ... interface{})
func (c *B) Fatalf (format string, args ... interface{})
func (c *B) Log (args ... interface{})
func (c *B) Logf (format string, args ... interface{})
func (c *B) Name () string
func (b *B) ReportAllocs ()
func (b *B) ResetTimer ()
func (b *B) Run (name string, f func (b *B)) bool
func (b *B) RunParallel (body func (*PB))
func (b *B) SetBytes (n int 64)
func (b *B) SetParallelism (p int)
func (c *B) Skip (args ... interface{})
func (c *B) SkipNow ()
func (c *B) Skipf (format string, args ... interface{})
func (c *B) Skipped () bool
func (b *B) StartTimer ()
func (b *B) StopTimer ()

基准测试示例

  • 我们为 split 包中的Split函数编写基准测试如下:
1
2
3
4
5
func BenchmarkSplit (b *testing. B) {
for i := 0; i < b.N; i++ {
Split ("沙河有沙又有河", "沙")
}
}
  • 基准测试并不会默认执行,需要增加-bench参数,所以我们通过执行go test -bench=Split命令执行基准测试,输出结果如下:
1
2
3
4
5
6
7
$ go test -bench=Split
goos: darwin
goarch: amd 64
pkg: github. com/Test/split
BenchmarkSplit-8 10000000 203 ns/op
PASS
ok github. com/Test/split 2.255 s
  • 其中BenchmarkSplit-8表示对 Split 函数进行基准测试,数字8表示GOMAXPROCS的值,这个对于并发基准测试很重要,10000000203 ns/op表示每次调用Split函数耗时203 ns,这个结果是10000000次调用的平均值。
  • 我们还可以为基准测试添加-benchmem参数,来获得内存分配的统计数据。
1
2
3
4
5
6
7
$ go test -bench=Split -benchmem
goos: darwin
goarch: amd 64
pkg: github. com/Test/split
BenchmarkSplit-8 10000000 215 ns/op 112 B/op 3 allocs/op
PASS
ok github. com/Test/split 2.394 s
  • 其中,112 B/op表示每次操作内存分配了 112 字节,3 allocs/op则表示每次操作进行了 3 次内存分配。

性能比较函数

  • 上面的基准测试只能得到给定操作的绝对耗时,但是在很多性能问题是发生在两个不同操作之间的相对耗时,比如同一个函数处理 1000 个元素的耗时与处理 1 万甚至 100 万个元素的耗时的差别是多少?再或者对于同一个任务究竟使用哪种算法性能最佳?我们通常需要对两个不同算法的实现使用相同的输入来进行基准比较测试。
  • 性能比较函数通常是一个带有参数的函数,被多个不同的 Benchmark 函数传入不同的值来调用,举个例子如下:
1
2
3
4
func benchmark (b *testing. B, size int){/* ... */}
func Benchmark 10 (b *testing. B){ benchmark (b, 10) }
func Benchmark 100 (b *testing. B){ benchmark (b, 100) }
func Benchmark 1000 (b *testing. B){ benchmark (b, 1000) }
  • 例如我们编写了一个计算斐波那契数列的函数如下:
1
2
3
4
5
6
7
8
9
// fib. go

// Fib 是一个计算第 n 个斐波那契数的函数。
func Fib (n int) int {
if n < 2 {
return n
}
return Fib (n-1) + Fib (n-2)
}
  • 我们编写的性能比较函数如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// fib_test. go

func benchmarkFib (b *testing. B, n int) {
for i := 0; i < b.N; i++ {
Fib (n)
}
}

func BenchmarkFib 1 (b *testing. B) { benchmarkFib (b, 1) }
func BenchmarkFib 2 (b *testing. B) { benchmarkFib (b, 2) }
func BenchmarkFib 3 (b *testing. B) { benchmarkFib (b, 3) }
func BenchmarkFib 10 (b *testing. B) { benchmarkFib (b, 10) }
func BenchmarkFib 20 (b *testing. B) { benchmarkFib (b, 20) }
func BenchmarkFib 40 (b *testing. B) { benchmarkFib (b, 40) }
  • 运行基准测试:
1
2
3
4
5
6
7
8
9
10
11
12
$ go test -bench=.
goos: darwin
goarch: amd 64
pkg: github. com/Test/fib
BenchmarkFib 1-8 1000000000 2.03 ns/op
BenchmarkFib 2-8 300000000 5.39 ns/op
BenchmarkFib 3-8 200000000 9.71 ns/op
BenchmarkFib 10-8 5000000 325 ns/op
BenchmarkFib 20-8 30000 42460 ns/op
BenchmarkFib 40-8 2 638524980 ns/op
PASS
ok github. com/Test/fib 12.944 s
  • 注意:默认情况下,每个基准测试至少运行 1 秒,如果在 Benchmark 函数返回时没有到 1 秒,则b.N的值会按 1,2,5,10,20,50,…增加,并且函数再次运行。
  • 最终的 BenchmarkFib 40 只运行了两次,每次运行的平均值只有不到一秒,像这种情况下我们应该可以使用-benchtime标志增加最小基准时间,以产生更准确的结果,例如:
1
2
3
4
5
6
7
$ go test -bench=Fib 40 -benchtime=20 s
goos: darwin
goarch: amd 64
pkg: github. com/Test/fib
BenchmarkFib 40-8 50 663205114 ns/op
PASS
ok github. com/Test/fib 33.849 s
  • 这一次BenchmarkFib 40函数运行了 50 次,结果就会更准确一些了。
  • 使用性能比较函数做测试的时候一个容易犯的错误就是把b.N作为输入的大小,例如以下两个例子都是错误的示范:
1
2
3
4
5
6
7
8
9
10
11
// 错误示范 1
func BenchmarkFibWrong (b *testing. B) {
for n := 0; n < b.N; n++ {
Fib (n)
}
}

// 错误示范 2
func BenchmarkFibWrong 2 (b *testing. B) {
Fib (b.N)
}

重置时间

  • b.ResetTimer之前的处理不会放到执行时间里,也不会输出到报告中,所以可以在之前做一些不计划作为测试报告的操作,例如:
1
2
3
4
5
6
7
func BenchmarkSplit (b *testing. B) {
time.Sleep (5 * time. Second) // 假设需要做一些耗时的无关操作。
b.ResetTimer () // 重置计时器。
for i := 0; i < b.N; i++ {
Split ("沙河有沙又有河", "沙")
}
}

并行测试

  • func (b *B) RunParallel (body func (*PB))会以并行的方式执行给定的基准测试。
  • RunParallel会创建出多个goroutine,并将b.N分配给这些goroutine执行,其中goroutine数量的默认值为GOMAXPROCS,用户如果想要增加非 CPU 受限(non-CPU-bound)基准测试的并行性,那么可以在RunParallel之前调用SetParallelism,RunParallel通常会与-cpu标志一同使用。
1
2
3
4
5
6
7
8
func BenchmarkSplitParallel (b *testing. B) {
b.SetParallelism (1) // 设置使用的 CPU 数。
b.RunParallel (func (pb *testing. PB) {
for pb.Next () {
Split ("沙河有沙又有河", "沙")
}
})
}
  • 执行一下基准测试:
1
2
3
4
5
6
7
8
$ go test -bench=.
goos: darwin
goarch: amd 64
pkg: github. com/Test/split
BenchmarkSplit-8 10000000 131 ns/op
BenchmarkSplitParallel-8 50000000 36.1 ns/op
PASS
ok github. com/Test/split 3.308 s
  • 还可以通过在测试命令后添加-cpu参数如go test -bench=. -cpu 1来指定使用的 CPU 数量。

示例函数

示例函数的格式

  • go test特殊对待的第三种函数就是示例函数,它们的函数名以Example为前缀,它们既没有参数也没有返回值,标准格式如下:
1
2
3
func ExampleName () {
// ...
}

示例函数示例

  • 下面的代码是我们为Split函数编写的一个示例函数:
1
2
3
4
5
6
7
func ExampleSplit () {
fmt.Println (split.Split ("a:b: c", ": "))
fmt.Println (split.Split ("沙河有沙又有河", "沙"))
// Output:
// [a b c]
// [ 河有又有河]
}
  • 为你的代码编写示例代码有如下三个用处:

    1. 示例函数能够作为文档直接使用,例如基于 web 的 godoc 中能把示例函数与对应的函数或包相关联。

    2. 示例函数只要包含了// Output:也是可以通过go test运行的可执行测试。

      1
      2
      3
      $ go test -run Example
      PASS
      ok github. com/Test/split 0.006 s
    3. 示例函数提供了可以直接运行的示例代码,可以直接在golang. orggodoc文档服务器上使用Go Playground运行示例代码,下图为strings. ToUpper函数在 Playground 的示例函数效果, Go Playground

Setup 与 TearDown

  • 测试程序有时需要在测试之前进行额外的设置(setup)或在测试之后进行拆卸(teardown)

TestMain

  • 通过在*_test. go文件中定义TestMain函数来可以在测试之前进行额外的设置(setup)或在测试之后进行拆卸(teardown)操作。
  • 如果测试文件包含函数:func TestMain (m *testing. M)那么生成的测试会先调用 TestMain (m),然后再运行具体测试,TestMain运行在主goroutine中,可以在调用 m.Run前后做任何设置(setup)和拆卸(teardown),退出测试的时候应该使用m.Run的返回值作为参数调用os. Exit
  • 一个使用TestMain来设置 Setup 和 TearDown 的示例如下:
1
2
3
4
5
6
7
func TestMain (m *testing. M) {
fmt.Println ("write setup code here...") // 测试之前的做一些设置。
// 如果 TestMain 使用了 flags,这里应该加上 flag.Parse ()
retCode := m.Run () // 执行测试。
fmt.Println ("write teardown code here...") // 测试之后做一些拆卸工作。
os.Exit (retCode) // 退出测试。
}
  • 需要注意的是:在调用TestMain时, flag. Parse并没有被调用,所以如果TestMain 依赖于 command-line 标志(包括 testing 包的标记),则应该显示的调用flag. Parse

子测试的 Setup 与 Teardown

  • 有时候我们可能需要为每个测试集设置 Setup 与 Teardown,也有可能需要为每个子测试设置 Setup 与 Teardown,下面我们定义两个函数工具函数如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 测试集的 Setup 与 Teardown
func setupTestCase (t *testing. T) func (t *testing. T) {
t.Log ("如有需要在此执行:测试之前的 setup")
return func (t *testing. T) {
t.Log ("如有需要在此执行:测试之后的 teardown")
}
}

// 子测试的 Setup 与 Teardown
func setupSubTest (t *testing. T) func (t *testing. T) {
t.Log ("如有需要在此执行:子测试之前的 setup")
return func (t *testing. T) {
t.Log ("如有需要在此执行:子测试之后的 teardown")
}
}
  • 使用方式如下:
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
func TestSplit (t *testing. T) {
type test struct { // 定义 test 结构体。
input string
sep string
want []string
}
tests := map[string]test{ // 测试用例使用 map 存储。
"simple": {input: "a:b: c", sep: ": ", want: []string{"a", "b", "c"}},
"wrong sep": {input: "a:b: c", sep: ",", want: []string{"a:b: c"}},
"more sep": {input: "abcd", sep: "bc", want: []string{"a", "d"}},
"leading sep": {input: "沙河有沙又有河", sep: "沙", want: []string{"", "河有", "又有河"}},
}
teardownTestCase := setupTestCase (t) // 测试之前执行 setup 操作。
defer teardownTestCase (t) // 测试之后执行 testdown 操作。

for name, tc := range tests {
t.Run (name, func (t *testing. T) { // 使用t.Run ()执行子测试。
teardownSubTest := setupSubTest (t) // 子测试之前执行 setup 操作。
defer teardownSubTest (t) // 测试之后执行 testdoen 操作。
got := Split (tc. input, tc. sep)
if !reflect.DeepEqual (got, tc. want) {
t.Errorf ("expected:% #v , got:% #v ", tc. want, got)
}
})
}
}
  • 测试结果如下:
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
$ go test -v
=== RUN TestSplit
=== RUN TestSplit/simple
=== RUN TestSplit/wrong_sep
=== RUN TestSplit/more_sep
=== RUN TestSplit/leading_sep
--- PASS: TestSplit (0.00 s)
split_test. go:71:如有需要在此执行:测试之前的 setup
--- PASS: TestSplit/simple (0.00 s)
split_test. go:79:如有需要在此执行:子测试之前的 setup
split_test. go:81:如有需要在此执行:子测试之后的 teardown
--- PASS: TestSplit/wrong_sep (0.00 s)
split_test. go:79:如有需要在此执行:子测试之前的 setup
split_test. go:81:如有需要在此执行:子测试之后的 teardown
--- PASS: TestSplit/more_sep (0.00 s)
split_test. go:79:如有需要在此执行:子测试之前的 setup
split_test. go:81:如有需要在此执行:子测试之后的 teardown
--- PASS: TestSplit/leading_sep (0.00 s)
split_test. go:79:如有需要在此执行:子测试之前的 setup
split_test. go:81:如有需要在此执行:子测试之后的 teardown
split_test. go:73:如有需要在此执行:测试之后的 teardown
=== RUN ExampleSplit
--- PASS: ExampleSplit (0.00 s)
PASS
ok github. com/Test/split 0.006 s

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!