go的测试
# Go 的测试
主要是《The Go Programming Language》 (opens new window)的
go test
学习笔记。
go 使用 go test
命令启动 go 的测试样例,go 的测试代码存放在以_test.go
为后缀名的文件里。在包目录内,所有以_test.go
为后缀名的源文件在执行go build
时不会被构建成包的一部分,它们是go test
测试的一部分。
在*_test.go
文件中,有三种类型的函数:
测试函数:函数名以
Test
为前缀,用于测试程序的一些逻辑行为是否正确。例如func TestSplit(t *testings.T)
。基准测试(benchmark)函数:基准测试函数是以
Benchmark
为函数名前缀的函数,它们用于衡量一些函数的性能。例如func BenchmarkSplit(t *testing.B)
。示例函数:示例函数是以
Example
为函数名前缀的函数,提供一个由编译器保证正确性的示例文档。例如func ExampleSplit()
。
go test
命令会遍历所有的*_test.go
文件中符合上述命名规则的函数,生成一个临时的main
包用于调用相应的测试函数,接着构建并运行、报告测试结果,最后清理测试中生成的临时文件。
# 测试函数
每个测试函数必须导入testing
包。测试函数有如下的签名:
func TestName(t *testing.T) {
// 测试用代码
}
2
3
测试函数的名字必须以Test
开头,可选的后缀名必须以大写字母开头:
func TestSin(t *testing.T) { /* ... */ }
func TestCos(t *testing.T) { /* ... */ }
func TestLog(t *testing.T) { /* ... */ }
2
3
其中t
参数用于报告测试失败和附加的日志信息。
下面我们以快速排序进行go test
示范。
这里初始化一个qsort
模块:go mod init qsort
。
sort/quick_sort.go
package quick_sort
// QuickSort 快速排序, arr 为 int 类型的切片
func QuickSort(arr []int) {
quickSortR(arr, 0, len(arr)-1)
}
func quickSortR(arr []int, p, r int) {
if p >= r {
return
}
q := partition(arr, p, r)
quickSortR(arr, p, q-1)
quickSortR(arr, q+1, r)
}
// partition 这里使用最普通的双指针法获取分区
func partition(arr []int, p, r int) int {
i, j := p, r-1
for i <= j {
if arr[i] < arr[r] {
i++
continue
}
if arr[j] >= arr[r] {
j--
continue
}
arr[i], arr[j] = arr[j], arr[i]
}
arr[i], arr[r] = arr[r], arr[i]
return i
}
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
在相同的目录下,quick_sort_test.go
测试文件中包含了TestQuickSort
测试函数。每一个都是测试QuickSort
是否给出正确的结果,并使用t.Error
报告失败信息:
sort/quick_sort_test.go
package quick_sort
import "testing"
// 普通切片
func TestQuickSort(t *testing.T) {
numbers := []int{4, 1, 2, 3, 5}
QuickSort(numbers)
want := []int{1, 2, 3, 4, 5}
if !equal(numbers, want) {
t.Errorf(`want: %v actual: %v`, want, numbers)
}
}
// 带重复数字的切片
func TestQuickSortDuplicate(t *testing.T) {
numbers := []int{6, 6, 2, 3, 5}
QuickSort(numbers)
want := []int{2, 3, 5, 6, 6}
if !equal(numbers, want) {
t.Errorf(`want: %v actual: %v`, want, numbers)
}
}
// 空切片
func TestQuickSortEmpty(t *testing.T) {
var numbers []int
QuickSort(numbers)
var want []int
if !equal(numbers, want) {
t.Errorf(`want: %v actual: %v`, want, numbers)
}
}
func equal(arr1, arr2 []int) bool {
if len(arr1) != len(arr2) {
return false
}
for i, v := range arr1 {
if v != arr2[i] {
return false
}
}
return true
}
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
44
45
46
go test
命令如果没有参数指定包那么将默认采用当前目录对应的包(和go build
命令一样)。
# ll
total 16
drwxr-xr-x 2 root root 4096 Dec 3 20:50 ./
drwxr-xr-x 3 root root 4096 Dec 3 17:52 ../
-rw-r--r-- 1 root root 525 Dec 3 20:37 quick_sort.go
-rw-r--r-- 1 root root 400 Dec 3 20:50 quick_sort_test.go
LAPTOP-7NBAJ7KH# go test
PASS
ok qsort 0.002s
2
3
4
5
6
7
8
9
参数-v
可用于打印每个测试函数的名字和运行时间:
# go test -v
=== RUN TestQuickSort
--- PASS: TestQuickSort (0.00s)
=== RUN TestQuickSortDuplicate
--- PASS: TestQuickSortDuplicate (0.00s)
=== RUN TestQuickSortEmpty
--- PASS: TestQuickSortEmpty (0.00s)
PASS
ok qsort 0.001s
2
3
4
5
6
7
8
9
参数-run
对应一个正则表达式,只有测试函数名被它正确匹配的测试函数才会被go test
测试命令运行:
# go test -v -run="Duplicate|Empty"
=== RUN TestQuickSortDuplicate
--- PASS: TestQuickSortDuplicate (0.00s)
=== RUN TestQuickSortEmpty
--- PASS: TestQuickSortEmpty (0.00s)
PASS
ok qsort 0.001s
2
3
4
5
6
7
将之前的所有测试数据合并到了一个测试中的表格中进行测试:
func TestQuickSort(t *testing.T) {
testCases := []struct {
name string
arr []int
// 测试预期的结果
want []int
}{
{
name: "normal",
arr: []int{4, 1, 2, 3, 5},
want: []int{1, 2, 3, 4, 5},
},
{
name: "duplicate",
arr: []int{6, 6, 2, 3, 5},
want: []int{2, 3, 5, 6, 6},
},
{
name: "empty",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
QuickSort(tc.arr)
if !equal(tc.arr, tc.want) {
t.Errorf(`want: %v actual: %v`, tc.want, tc.arr)
}
})
}
}
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
现在所有的测试都通过了:
# go test -v -run="Duplicate|Empty"
=== RUN TestQuickSortDuplicate
--- PASS: TestQuickSortDuplicate (0.00s)
=== RUN TestQuickSortEmpty
--- PASS: TestQuickSortEmpty (0.00s)
PASS
ok qsort 0.001s
2
3
4
5
6
7
这种表格驱动的测试在Go语言中很常见。可以很容易地向表格添加新的测试数据,并且后面的测试逻辑也没有冗余,这样我们可以有更多的精力去完善错误信息。
# 随机测试
表格驱动的测试便于构造基于精心挑选的测试数据的测试用例。另一种测试思路是随机测试,也就是通过构造更广泛的随机输入来测试探索函数的行为。
randomSlice
函数用于随机生成切片:
func TestQuickSort_ByRandom(t *testing.T) {
seed := time.Now().UTC().UnixNano()
t.Logf("Random seed: %d", seed)
rng := rand.New(rand.NewSource(seed))
for i := 0; i < 1000; i++ {
arr, want := randomSlice(rng)
QuickSort(arr)
if !equal(arr, want) {
t.Errorf(`want: %v actual: %v`, want, arr)
}
}
}
func randomSlice(rng *rand.Rand) ([]int, []int) {
n := rng.Intn(25)
var numbers []int
for i := 0; i < n; i++ {
numbers = append(numbers, rng.Intn(100))
}
sortedNumbers := make([]int, len(numbers))
copy(sortedNumbers, numbers)
// 这里使用标准库的排序得到正确的排序结果
sort.Ints(sortedNumbers)
return numbers, sortedNumbers
}
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
# 测试一个命令
如果一个包的名字是main
,那么在构建时会生成一个可执行程序,不过main
包可以作为一个包被测试器代码导入。
echo.go
package main
import (
"flag"
"fmt"
"io"
"os"
"strings"
)
var (
n = flag.Bool("n", false, "omit trailing newline")
s = flag.String("s", " ", "separator")
)
var out io.Writer = os.Stdout
func main() {
flag.Parse()
if err := echo(!*n, *s, flag.Args()); err != nil {
_, err = fmt.Fprintf(os.Stderr, "echo: %v\n", err)
if err != nil {
os.Exit(1)
}
os.Exit(1)
}
}
func echo(newline bool, sep string, args []string) error {
_, err := fmt.Fprint(out, strings.Join(args, sep))
if err != nil {
return err
}
if newline {
_, err = fmt.Fprintln(out)
if err != nil {
return err
}
}
return nil
}
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
通过参数来减少echo
函数对全局变量的依赖。我们还增加了一个全局名为out
的变量来替代直接使用os.Stdout
,这样测试代码可以根据需要将out
修改为不同的对象以便于检查。下面就是echo_test.go
文件中的测试代码:
package main
import (
"bytes"
"fmt"
"testing"
)
func TestEcho(t *testing.T) {
testCases := []struct {
newline bool
sep string
args []string
want string
}{
{true, "", []string{}, "\n"},
{false, "", []string{}, ""},
{true, "\t", []string{"one", "two", "three"}, "one\ttwo\tthree\n"},
{true, ",", []string{"a", "b", "c"}, "a,b,c\n"},
{false, ":", []string{"1", "2", "3"}, "1:2:3"},
}
for _, tc := range testCases {
descr := fmt.Sprintf("echo(%v, %q, %q)",
tc.newline, tc.sep, tc.args)
// 增加了一个全局名为 out 的变量来替代直接使用 os.Stdout
out = new(bytes.Buffer)
// 直接传参测试
if err := echo(tc.newline, tc.sep, tc.args); err != nil {
t.Errorf("%s failed: %v", descr, err)
continue
}
got := out.(*bytes.Buffer).String()
if got != tc.want {
t.Errorf("%s = %q, want %q", descr, got, tc.want)
}
}
}
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
要注意的是测试代码和被测试代码在同一个包。虽然是main
包,也有对应的main
入口函数,但是在测试的时候main
包只是TestEcho
测试函数导入的一个普通包,里面main
函数并没有被导出,而是被忽略的。
# 外部测试包
有这样的两个包(注意这是故意设计的包):
animal
包,提供了Animal
接口和提供了一个Eat
函数;animal/dog
包,实现了animal
包里Animal
接口,同时Eat
方法引用了animal
包里的Eat
函数。
上层animal/dog
包依赖下层的animal
包。然后,animal
包中的某个测试演示了animal/dog
包中的方法。也就是说,一个下层包的测试代码导入了上层的包。
这样的行为在animal
包的测试代码中会导致包的循环依赖。
animal/animal.go
package animal
import "fmt"
type Animal interface {
Sleep()
Eat()
}
func Eat(food string) {
fmt.Printf("吃%s", food)
}
2
3
4
5
6
7
8
9
10
11
12
animal/animal_test.go
package animal
import (
"animal/dog"
"testing"
)
func TestEat(t *testing.T) {
testCases := []struct {
name string
animal Animal
}{
{
name: "dog",
animal: dog.Dog{},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tc.animal.Eat()
tc.animal.Sleep()
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
animal/dog/dog.go
package dog
import (
"animal"
"fmt"
)
type Dog struct {
}
func (Dog) Sleep() {
fmt.Println("狗狗在睡觉")
}
func (Dog) Eat() {
animal.Eat("狗粮")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
现在尝试执行admin_test.go
的测试函数:
go test --run=TestEat
# animal
package animal
imports animal/dog: import cycle not allowed in test
FAIL animal [setup failed]
2
3
4
5
发现测试失败:imports animal/dog: import cycle not allowed in test
。
解决方式:可以通过外部测试包的方式解决循环依赖的问题,也就是在animal
包所在的目录声明一个独立的animal_test
测试包(或者叫test
的测试包)。其中包名的_test
后缀告诉go test
工具它应该建立一个额外的包来运行测试。
把animal_test.go
移动到animal/animal_test/animal_test.go
,进入animal/animal_test
目录,然后再次执行go test
:
# go test --run=TestEat
吃狗粮狗狗在睡觉
PASS
ok animal/animal_test 0.001s
2
3
4
5
提示:测试文件可以放在一个与测试包同级目录下的testdata
目录。可以参考标准库net
下的testdata
目录。
# 测试覆盖率
go test
命令集成了测试覆盖率工具,可以用来度量我们的测试覆盖率。
sort/quick_sort_test.go
func TestCoverage(t *testing.T) {
testCases := []struct {
name string
arr []int
// 测试预期的结果
want []int
}{
{
name: "normal",
arr: []int{4, 1, 2, 3, 5},
want: []int{1, 2, 3, 4, 5},
},
{
name: "duplicate",
arr: []int{6, 6, 2, 3, 5},
want: []int{2, 3, 5, 6, 6},
},
{
name: "empty",
},
{
name: "reverse",
arr: []int{5, 4, 3, 2, 1},
want: []int{1, 2, 3, 4, 5},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
QuickSort(tc.arr)
if !equal(tc.arr, tc.want) {
t.Errorf(`want: %v actual: %v`, tc.want, tc.arr)
}
})
}
}
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
确保所有的测试条件都通过
# go test -v --run=Coverage === RUN TestCoverage === RUN TestCoverage/normal === RUN TestCoverage/duplicate === RUN TestCoverage/empty === RUN TestCoverage/reverse --- PASS: TestCoverage (0.00s) --- PASS: TestCoverage/normal (0.00s) --- PASS: TestCoverage/duplicate (0.00s) --- PASS: TestCoverage/empty (0.00s) --- PASS: TestCoverage/reverse (0.00s) PASS ok leanring-go/base/ch11/test_func 0.001s
1
2
3
4
5
6
7
8
9
10
11
12
13下面这个命令可以显示测试覆盖率工具的使用用法:
# go tool cover Usage of 'go tool cover': Given a coverage profile produced by 'go test': go test -coverprofile=c.out Open a web browser displaying annotated source code: go tool cover -html=c.out ...
1
2
3
4
5
6
7
8用
-coverprofile
标志参数重新运行测试:# go test --run=Coverage -coverprofile=c.out PASS coverage: 100.0% of statements ok qsort 0.001s
1
2
3
4这个标志参数通过在测试代码中插入生成钩子来统计覆盖率数据。也就是说,在运行每个测试前,它将待测代码拷贝一份并做修改,在每个词法块都会设置一个布尔标志变量。当被修改后的被测试代码运行退出时,将统计日志数据写入
c.out
文件,并打印一部分执行的语句的一个总结。如果你需要的是摘要,使用
go test -cover
:# go test -cover --run=Coverage PASS coverage: 100.0% of statements ok qsort 0.001s
1
2
3
4如果使用了
-covermode=count
标志参数,那么将在每个代码块插入一个计数器而不是布尔标志量。在统计结果中记录了每个块的执行次数,这可以用于衡量哪些是被频繁执行的热点代码。生成测试覆盖率
HTML
报告# go tool cover -html=c.out HTML output written to /tmp/cover2136979051/coverage.html
1
2在浏览器中打开。
# 基准测试
基准测试是测量一个程序在固定工作负载下的性能。
基准测试用例的定义如下:
func BenchmarkName(b *testing.B){
// ...
}
2
3
- 在 Go 语言中,基准测试函数和普通测试函数写法类似,但是以
Benchmark
为前缀名,并且带有一个*testing.B
类型的参数;*testing.B
参数除了提供和*testing.T
类似的方法,还有额外一些和性能测量相关的方法。它还提供了一个整数N
,用于指定操作执行的循环次数。 - 执行基准测试时,需要添加
-bench
参数。
下面是QuickSort
函数的基准测试,其中循环将执行N
次:
func BenchmarkQuickSort(b *testing.B) {
arr := []int{4, 1, 2, 3, 5}
for i := 0; i < b.N; i++ {
QuickSort(arr)
}
}
2
3
4
5
6
go test
命令默认情况下不运行任何基准测试。我们需要通过-bench
命令行标志参数手工指定要运行的基准测试函数。该参数是一个正则表达式,用于匹配要执行的基准测试函数的名字,默认值是空的。其中.
模式将可以匹配所有基准测试函数。
# go test -bench=.
goos: linux
goarch: amd64
pkg: qsort
cpu: AMD Ryzen 7 5800H with Radeon Graphics
BenchmarkQuickSort-16 58653486 20.17 ns/op
PASS
ok qsort 1.209s
2
3
4
5
6
7
8
测试结果说明:
BenchmarkQuickSort-16
:这里的16
指的是GOMAXPROCS
的值,这对于一些与并发相关的基准测试是重要的信息。58653486 20.17 ns/op
:每次调用QuickSort
函数花费20.17 ns
,是执行 58,653,486 次的平均时间。
如果在运行前基准测试需要一些耗时的配置,则可以使用 b.ResetTimer()
先重置定时器:
func BenchmarkXXX(b *testing.B) {
... // 耗时的操作
b.ResetTimer()
for i := 0; i < b.N; i++ {
...
}
}
2
3
4
5
6
7
-benchmem
命令行标志参数将在报告中包含内存的分配数据统计:
# go test -bench=. -benchmem
goos: linux
goarch: amd64
pkg: qsort
cpu: AMD Ryzen 7 5800H with Radeon Graphics
BenchmarkQuickSort-16 56495100 20.18 ns/op 0 B/op 0 allocs/op
PASS
ok qsort 1.167s
2
3
4
5
6
7
8
0 allocs/op
:每次调用QuickSort
函数的内存分配。0 B/op
:每次调用QuickSort
函数分配内存0 B
,是执行 56,495,100 次的平均时间。
使用 RunParallel
测试并发性能:
func BenchmarkQuickSort_Parallel(b *testing.B) {
arr := []int{4, 1, 2, 3, 5}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// 所有 goroutine 一起执行,循环一共执行 b.N 次
QuickSort(arr)
}
})
}
2
3
4
5
6
7
8
9