测试
为了确保软件开发的质量,开发人员需要借助测试来确保代码的正确性。
测试分为两步:
- 单元测试:对最小的可测试单元进行测试,通常是一个方法或者函数。
- 集成测试:软件整体性的测试或者多个组件模块合并在一起进行测试。
开发人员在开发完成后,应该进行单元测试,用于保证最小化单元的正确性
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
}
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)
}
})
}
}
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 | 详细输出:记录所有运行的测试。即使测试成功,也打印所有来自 Log 和 Logf 调用的文本。 |
-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)
}
}
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
来开启基准测试,默认不运行。
比如测试以下Sprintf
和Format
的性能:
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)
}
}
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
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
}
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)
}
}
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
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
}
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)
})
}
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)
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16