FeelingLife FeelingLife
首页
  • Go

    • Go基础知识
  • Python

    • Python进阶
  • 操作系统
  • 计算机网络
  • MySQL
  • 学习笔记
  • 常用到的算法
  • Docker
  • Kubernetes
  • Observability
  • 容器底层
其他技术
  • 友情链接
  • 收藏
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

xuqil

一介帆夫
首页
  • Go

    • Go基础知识
  • Python

    • Python进阶
  • 操作系统
  • 计算机网络
  • MySQL
  • 学习笔记
  • 常用到的算法
  • Docker
  • Kubernetes
  • Observability
  • 容器底层
其他技术
  • 友情链接
  • 收藏
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 环境部署

  • 测试

    • go的测试
      • 测试函数
        • 随机测试
        • 测试一个命令
        • 外部测试包
      • 测试覆盖率
      • 基准测试
  • 反射

  • 数据库操作

  • 并发编程

  • 内存管理

  • 线程安全

  • Go 技巧

  • middleware

  • 第三方库

  • Go各版本特性

  • 《go基础知识》
  • 测试
xuqil
2022-12-07
目录

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文件中,有三种类型的函数:

  1. 测试函数:函数名以Test为前缀,用于测试程序的一些逻辑行为是否正确。例如func TestSplit(t *testings.T)。

  2. 基准测试(benchmark)函数:基准测试函数是以Benchmark为函数名前缀的函数,它们用于衡量一些函数的性能。例如func BenchmarkSplit(t *testing.B)。

  3. 示例函数:示例函数是以Example为函数名前缀的函数,提供一个由编译器保证正确性的示例文档。例如func ExampleSplit()。

go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,生成一个临时的main包用于调用相应的测试函数,接着构建并运行、报告测试结果,最后清理测试中生成的临时文件。

# 测试函数

每个测试函数必须导入testing包。测试函数有如下的签名:

func TestName(t *testing.T) {
    // 测试用代码
}
1
2
3

测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头:

func TestSin(t *testing.T) { /* ... */ }
func TestCos(t *testing.T) { /* ... */ }
func TestLog(t *testing.T) { /* ... */ }
1
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
}
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

在相同的目录下,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
}

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
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
1
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
1
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
1
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)
			}
		})
	}
}
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

现在所有的测试都通过了:

# go test -v -run="Duplicate|Empty"
=== RUN   TestQuickSortDuplicate
--- PASS: TestQuickSortDuplicate (0.00s)
=== RUN   TestQuickSortEmpty
--- PASS: TestQuickSortEmpty (0.00s)
PASS
ok      qsort   0.001s
1
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
}
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

# 测试一个命令

如果一个包的名字是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
}
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

通过参数来减少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)
		}
	}
}
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

要注意的是测试代码和被测试代码在同一个包。虽然是main包,也有对应的main入口函数,但是在测试的时候main包只是TestEcho测试函数导入的一个普通包,里面main函数并没有被导出,而是被忽略的。

# 外部测试包

有这样的两个包(注意这是故意设计的包):

  • animal包,提供了Animal接口和提供了一个Eat函数;

  • animal/dog包,实现了animal包里Animal接口,同时Eat方法引用了animal包里的Eat函数。

上层animal/dog包依赖下层的animal包。然后,animal包中的某个测试演示了animal/dog包中的方法。也就是说,一个下层包的测试代码导入了上层的包。

image-20221207212000468

这样的行为在animal包的测试代码中会导致包的循环依赖。

animal/animal.go

package animal

import "fmt"

type Animal interface {
	Sleep()
	Eat()
}

func Eat(food string) {
	fmt.Printf("吃%s", food)
}
1
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()
		})
	}
}

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

animal/dog/dog.go

package dog

import (
	"animal"
	"fmt"
)

type Dog struct {
}

func (Dog) Sleep() {
	fmt.Println("狗狗在睡觉")
}

func (Dog) Eat() {
	animal.Eat("狗粮")
}
1
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]
1
2
3
4
5

发现测试失败:imports animal/dog: import cycle not allowed in test。

解决方式:可以通过外部测试包的方式解决循环依赖的问题,也就是在animal包所在的目录声明一个独立的animal_test测试包(或者叫test的测试包)。其中包名的_test后缀告诉go test工具它应该建立一个额外的包来运行测试。

image-20221207212000468

把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

1
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)
			}
		})
	}
}
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
  1. 确保所有的测试条件都通过

    # 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
  2. 下面这个命令可以显示测试覆盖率工具的使用用法:

    # 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
  3. 用-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标志参数,那么将在每个代码块插入一个计数器而不是布尔标志量。在统计结果中记录了每个块的执行次数,这可以用于衡量哪些是被频繁执行的热点代码。

  4. 生成测试覆盖率HTML报告

    # go tool cover -html=c.out
    HTML output written to /tmp/cover2136979051/coverage.html
    
    1
    2

    在浏览器中打开。

    image-20221207215842197

# 基准测试

基准测试是测量一个程序在固定工作负载下的性能。

基准测试用例的定义如下:

func BenchmarkName(b *testing.B){
    // ...
}
1
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)
	}
}
1
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
1
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++ {
		...
	}
}
1
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
1
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)
		}
	})
}
1
2
3
4
5
6
7
8
9
#测试
上次更新: 2024/05/29, 06:25:22
VS CODE 配置远程 Go 开发环境
反射

← VS CODE 配置远程 Go 开发环境 反射→

最近更新
01
VXLAN互通实验
05-13
02
VXLAN
05-13
03
VLAN
05-13
更多文章>
Theme by Vdoing | Copyright © 2018-2025 FeelingLife | 粤ICP备2022093535号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式