测试

为了确保软件开发的质量,开发人员需要借助测试来确保代码的正确性。

测试分为两步:

  • 单元测试:对最小的可测试单元进行测试,通常是一个方法或者函数。
  • 集成测试:软件整体性的测试或者多个组件模块合并在一起进行测试。

开发人员在开发完成后,应该进行单元测试,用于保证最小化单元的正确性

1. 单元测试

单元测试(unit testing)是软件开发过程中必不可少的一个步骤,是对软件中最小可测试单元进行检查和验证,在Go语言中一般指方法或者函数

大家可以想象一下以下场景:

  • 只修改了一处代码,上线后,导致项目大面积崩溃。
  • 其他同事修改了你的一处代码,上线后,导致应用挂掉。
  • 做了一处性能优化,上线后导致问题或者要进行成本很大的线上测试。
  • 写了几千行的代码,在做集成测试时发现了问题,浪费了好几天的时间进行调试(可能最后发现只是某一个地方的单词写错了)。
  • 写完代码才发现业务流程设计不合理。
  • 提交代码后,导致其他同事的代码不能运行
  • 等等

开发人员在写完代码后,必须进行测试后,才能提交代码

1.1 Go语言单元测试

Golang提供了语言级别的单元测试,通过引入testing包和执行go test命令来实现单元测试。

  • 测试文件命名以xxx_test.go命名。(会被go test识别,并且不会被go build编译)
  • 测试方法以Test[^a-z]开头,命名符合驼峰或者snake(下划线)风格,建议风格保持统一。
  • 测试方法参数为 t *testing.T b *testing.B f *testing.F
  • 测试文件和被测试文件必须在一个包中。

案例:

func Add(a, b int) int {
	return a + b
}
1
2
3
//这是自动生成的测试用例代码,采用了`Table Driven`的组织方式
func TestAdd(t *testing.T) {
	type args struct {
		a int
		b int
	}
	tests := []struct {
		name string
		args args
		want int
	}{
		{
			name: "positive number",
			args: args{
				a: 1,
				b: 2,
			},
			want: 3,
		},
		{
			name: "negative number",
			args: args{
				a: -1,
				b: -2,
			},
			want: -3,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := Add(tt.args.a, tt.args.b); got != tt.want {
				t.Errorf("Add() = %v, want %v", got, tt.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
  • go test 会执行该包下面的所有测试用例
  • -v会显示每个用例的测试结果
  • -cover会显示测试覆盖率(测试代码是否覆盖了代码的所有逻辑)
  • -run TestAdd可以指定要运行的测试用例,支持部分正则表达式
  • -count 运行测试的次数, go test -count 2
  • -timeout 设置运行测试的超时时间,go test -timeout 1s

填充的数据,我们可以借助各种AI助理,快速生成填充数据,比如chatgpt

1.2 Table Driven

表驱动法(Table-Driven Method)是一种基于数据表的程序设计方法。

Golang采用了Table-Driven组织测试。

Table Driven主要分成三个部分:

  • 测试用例的定义:即每一个测试用例需要有什么。
  • 具体的测试用例:你设计的每一个测试用例都在这里。
  • 执行测试用例:这里面还包括了对测试结果进行断言。

了解即可,测试用例并不强求使用Table Driven,只是建议。

1.3 go test命令

go test命令详情:

参数说明
-bench regexp仅运行与正则表达式匹配的基准测试。默认不运行任何基准测试。使用 -bench .-bench= 来运行所有基准测试。
-benchtime t运行每个基准测试足够多的迭代,以达到指定的时间 t(例如 -benchtime 1h30s)。默认为1秒(1s)。特殊语法 Nx 表示运行基准测试 N 次(例如 -benchtime 100x)。
-count n运行每个测试、基准测试和模糊测试 n 次(默认为1次)。如果设置了 -cpu,则为每个 GOMAXPROCS 值运行 n 次。示例总是运行一次。-count 不适用于通过 -fuzz 匹配的模糊测试。
-cover启用覆盖率分析。
-covermode set,count,atomic设置覆盖率分析的 mode。默认为 "set",如果启用了 -race,则为 "atomic"。
-coverpkg pattern1,pattern2,pattern3对匹配模式的包应用覆盖率分析。默认情况下,每个测试仅分析正在测试的包。
-cpu 1,2,4指定一系列的 GOMAXPROCS 值,在这些值上执行测试、基准测试或模糊测试。默认为当前的 GOMAXPROCS 值。-cpu 不适用于通过 -fuzz 匹配的模糊测试。
-failfast在第一个测试失败后不启动新的测试。
-fullpath在错误消息中显示完整的文件名。
-fuzz regexp运行与正则表达式匹配的模糊测试。当指定时,命令行参数必须精确匹配主模块中的一个包,并且正则表达式必须精确匹配该包中的一个模糊测试。
-fuzztime t在模糊测试期间运行足够多的模糊目标迭代,以达到指定的时间 t(例如 -fuzztime 1h30s)。默认为永远运行。特殊语法 Nx 表示运行模糊目标 N 次(例如 -fuzztime 1000x)。
-fuzzminimizetime t在每次最小化尝试期间运行足够多的模糊目标迭代,以达到指定的时间 t(例如 -fuzzminimizetime 30s)。默认为60秒。特殊语法 Nx 表示运行模糊目标 N 次(例如 -fuzzminimizetime 100x)。
-json以 JSON 格式记录详细输出和测试结果。这以机器可读的格式呈现 -v 标志的相同信息。
-list regexp列出与正则表达式匹配的测试、基准测试、模糊测试或示例。不会运行任何测试、基准测试、模糊测试或示例。
-parallel n允许并行执行调用 t.Parallel 的测试函数,以及运行种子语料库时的模糊目标。此标志的值是同时运行的最大测试数。
-run regexp仅运行与正则表达式匹配的测试、示例和模糊测试。
-short告诉长时间运行的测试缩短其运行时间。默认情况下是关闭的,但在 all.bash 中设置,以便在安装 Go 树时可以运行健全性检查,但不花费时间运行详尽的测试。
-shuffle off,on,N随机化测试和基准测试的执行顺序。默认情况下是关闭的。如果 -shuffle 设置为 on,则使用系统时钟种子随机化器。如果 -shuffle 设置为整数 N,则 N 将用作种子值。在这两种情况下,种子将报告以便复现。
-skip regexp仅运行与正则表达式不匹配的测试、示例、模糊测试和基准测试。
-timeout d如果测试二进制文件运行时间超过持续时间 d,则发生 panic。如果 d 为0,则禁用超时。默认为10分钟(10m)。
-v详细输出:记录所有运行的测试。即使测试成功,也打印所有来自 LogLogf 调用的文本。
-vet list配置在 "go test" 期间对 "go vet" 的调用,以使用由逗号分隔的 vet 检查列表。如果列表为空,"go test" 使用被认为总是值得解决的精选检查列表运行 "go vet"。如果列表为

可以通过go help test查看帮助信息。

1.4 Helper

t.Helper()用于标注当前的函数为测试辅助函数

在编写一些复杂的测试代码时,可能需要调用一些辅助函数用于设置或验证测试条件,这些辅助函数本身并不是测试的主要部分,go test运行输出中,会忽略辅助函数

比如:

// 这是一个测试辅助函数,它设置了测试需要的一些初始条件
func setupTest(t *testing.T) (int, int) {
	t.Helper() // 标记这个函数为测试辅助函数
	// 这里可以是任何设置测试环境的代码
	t.Errorf("setupTest error")
	return 10, 20 // 例如,返回两个用于测试的整数
}

// 这是一个测试函数
func TestAdd(t *testing.T) {
	a, b := setupTest(t) // 调用测试辅助函数来设置测试数据
	got := Add(a, b)
	want := 30
	if got != want {
		t.Errorf("Add(%d, %d) = %d; want %d", a, b, got, want)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

测试发生错误后,并不会输出setupTest的调用信息,而是显示错误位置在TestAdd中,这也有个好处,当有多个测试函数调用helper函数时,可以准确的知道发生错误的是哪个测试函数。

**注意:**辅助函数最好不要返回error

2. 基准测试

Benchmark开头的测试函数为基准测试函数,用于测试一段程序运行时的性能。

  • 参数为*testing.B

使用go test -bench 来开启基准测试,默认不运行。

比如测试以下SprintfFormat的性能:

func BenchmarkSprintf(b *testing.B) {
	num := 10
    //重置计时器,确保从一个一致的状态开始
    //基准测试函数 必须
    //在真正执行时重置,可以忽略准备期的开销
	b.ResetTimer()
    //b.N就是执行的次数
	for i := 0; i < b.N; i++ {
		fmt.Sprintf("%d", num)
	}
}

func BenchmarkFormat(b *testing.B) {
	num := int64(10)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		strconv.FormatInt(num, 10)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

运行go test -bench .,得到以下结果:

goos: windows //操作系统
goarch: amd64 /cpu架构
pkg: testLearn // 当前目录
cpu: Intel(R) Core(TM) i9-14900K //CPU信息
BenchmarkSprintf-32     42520923                27.78 ns/op
BenchmarkFormat-32      1000000000               0.9868 ns/op
PASS
1
2
3
4
5
6
7
  • 32代表GOMAXPROCS即最大CPU核心数,即P的数量,当前CPU是24核32线程。
  • 42520923: 表示迭代次数
  • 27.78 ns/op:表示每次花费的纳秒

CPU数可以使用 -cpu=num选项进行调整,执行的次数可以通过-benchtime=100x这样进行指定

2.1 内存测试

在一些情况下,我们想要测试内存占用,可以加-benchmem选项

比如:

func newSlice(n int) []int {
	rand.Seed(time.Now().UnixNano())
	// 注意,这里在生成切片的时候并没有指定容量
	nums := make([]int, 0)
	for i := 0; i < n; i++ {
		nums = append(nums, rand.Int())
	}
	return nums
}
func newSliceWithCap(n int) []int {
	rand.Seed(time.Now().UnixNano())
	// 注意,这里在生成切片的时候指定了容量
	nums := make([]int, 0, n)
	for i := 0; i < n; i++ {
		nums = append(nums, rand.Int())
	}
	return nums
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func BenchmarkNewSlice(b *testing.B) {
	for n := 0; n < b.N; n++ {
		newSlice(1000000)
	}
}

func BenchmarkNewSliceWithCap(b *testing.B) {
	for n := 0; n < b.N; n++ {
		newSliceWithCap(1000000)
	}
}
1
2
3
4
5
6
7
8
9
10
11
D:\go\project\hello\testLearn> go test -benchmem -bench .
goos: windows
goarch: amd64
pkg: testLearn
cpu: Intel(R) Core(TM) i9-14900K
BenchmarkNewSlice-32                  85(运行次数)          14064528 ns/op  (每次运行时间)     41678148 B/op (每次操作分配的内存,字节单位)        38 allocs/op(每次操作进行内存分配的次数)
BenchmarkNewSliceWithCap-32          100          11062605 ns/op         8003586 B/op          1 allocs/op
PASS

1
2
3
4
5
6
7
8
9

为了更准确的测试性能,go提供了控制计时器:

  • b.ResetTimer():重置计时
  • b.StopTimer(): 停止计时
  • b.StartTimer():开始计时

3. 模糊测试

模糊测试就是输入一些非预期的输入并监测异常结果来发现问题,Golang已经在1.18版本中将模糊测试添加进了标准库。

比如:

func Div(a, b int) int {
    //如果测试的数据b忽略了0,那么就会埋下隐患
	return a / b
}
1
2
3
4
//go test  -fuzz .
//运行模糊测试,会发现b为0的异常
func FuzzDiv(f *testing.F) {
	f.Fuzz(func(t *testing.T, a, b int) {
		Div(a, b)
	})
}
1
2
3
4
5
6
7

go的模糊测试允许提供一些初始语料,比如:

func FuzzDiv(f *testing.F) {
	testcases := []struct {
		a, b int
	}{
		{10, 2},
		{5, 3},
		{-6, 3},
		{-6, -3},
	}
	for _, v := range testcases {
		f.Add(v.a, v.b)
	}
	f.Fuzz(func(t *testing.T, a, b int) {
		Div(a, b)
	})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16