20 | 如何添加单元测试用例
提示:
- 所有体系课见专栏:Go 项目开发极速入门实战课;
- 欢迎加入我的训练营:云原生AI实战营,一个助力 Go 开发者在 AI 时代建立技术竞争力的实战营;
- 本节课最终源码位于 fastgo 项目的 feature/s16 分支;
- 更详细的课程版本见:Go 项目开发中级实战课:33 | 项目测试:如何开发单元测试用例?
在实际开发中,不仅要开发功能,而且还要确保这些功能稳定可靠,并且拥有一个不错的性能,要确保这些,就要对代码进行测试。测试分为很多种,例如:功能测试、性能测试、集成测试、端到端测试、单元测试等。
对于开发者来说,需要执行的测试种类一般是单元测试和性能测试。除此之外,Go 还提供了其他类型的测试,例如:模糊测试、示例测试。本节课会详细介绍开发者需要重点关注的单元测试和性能测试用例。
Go 标准库 testing 包介绍
Go 语言自带测试框架 testing,可以用于编写单元测试用例和性能测试用例,并通过 go test
命令运行测试用例。
go test
命令在运行测试用例时,以 Go 包为单位进行测试。运行时需要指定包名,例如:go test <包名>
。如果未指定包名,测试将默认作用于运行命令时所在的包。go test
执行时会遍历以 _test.go
结尾的源码文件,并运行其中以 Test
、Benchmark
、Example
、Fuzz
开头的测试函数。这些源码文件需满足以下规则:
- 文件名要求: 文件名必须以
_test.go
结尾,且建议与被测试的源文件位于同一个包中; - 测试用例函数规范: 测试用例函数需以
Test
、Benchmark
、Example
、Fuzz
开头; - 测试执行顺序: 测试用例的执行顺序按照源码中的定义顺序依次进行;
- 单元测试函数: 函数名称形如
TestXxx(t *testing.T)
,其中Xxx
部分为任意字母数字组合,首字母需大写。例如:Testlogger
是错误的函数名,TestLogger
是正确的函数名。参数testing.T
可以用于记录错误或测试状态; - 性能测试函数: 函数名称形如
BenchmarkXxx (b *testing.B)
,函数以b.N
作为循环次数,其中N
值会动态变化; - 示例函数: 示例函数名称形如
ExampleXxx()
,没有参数,执行后将其输出与注释// Output:
中声明的结果进行对比。
testing.T
提供了丰富的方法来管理测试过程和结果,常用方法如下:
- 输出测试信息:
t.Log
、t.Logf
两个方法可以用来输出测试信息; - 输出测试失败信息:
t.Error
、t.Errorf
两个方法可以用来输出测试异常或失败时的信息; - 记录致命错误:
t.Fatal
、t.Fatalf
两个方法用来记录致命错误,并退出测试; - 标记测试失败:
t.Fail
方法用来将当前测试标记为失败,但测试不会退出。t.Failed
方法用来检查当前测试是否已标记为失败; - 终止测试:
t.FailNow
用于标记当前测试失败,并立即终止当前测试函数的执行; - 跳过测试:
t.Skip
、t.Skipf
两个方法可用于跳过当前测试函数的执行,并记录一条备注信息。t.Skipped
方法可用于检测当前测试是否已被跳过; - 并行执行测试:
t.Parallel
可将测试标记为支持并行运行。
性能测试过程中,需要重点注意 BenchmarkXxx
函数,其参数 testing.B
用于设置动态变化的循环次数(b.N
值),例如:
func BenchmarkResourceID_New(b *testing.B) {// 性能测试b.ResetTimer()for i := 0; i < b.N; i++ {userID := rid.UserID_ = userID.New(uint64(i))}
}
Go 测试用例编写
在实际项目开发中,最常编写的是单元测试用例,其次是性能测试用例。在某些场景下,还可能需要编写模糊测试用例和示例测试用例。本节会详细介绍如何编写单元测试、性能测试。
单元测试
新建 internal/pkg/rid/rid_test.go 文件,在文件中添加 ResourceID 数据类型 String()
方法的单元测试用例:
func TestResourceID_String(t *testing.T) {// 测试 UserID 转换为字符串userID := rid.UserIDassert.Equal(t, "user", userID.String(), "UserID.String() should return 'user'")// 测试 PostID 转换为字符串postID := rid.PostIDassert.Equal(t, "post", postID.String(), "PostID.String() should return 'post'")
}
在编写单元测试用例时,经常需要对比期望值和实际值是否一致,可以直接编写代码来对比,例如:
if expected != actual {t.Error("actual value did not match the expected value")
}
但更建议使用优秀的断言包来对比。在 Go 生态中,比较常用的断言包是 github.com/stretchr/testify/assert。
assert 包,提供了一组丰富的断言函数,用于简化 Go 语言中的单元测试用例编写。通过 assert 包,开发者可以用更直观的方式验证测试结果,如检查值相等、布尔值匹配、集合包含关系、错误状态等,从而极大地提测试代码的可读性和开发效率。
实现单元测试用例后,可以通过执行 go test
命令运行测试用例。go test
命令支持不同的命令行选项,从而实现多种测试效果。常用的 go test
命令如下。
(1)执行默认的测试用例
在 internal/pkg/rid 目录下执行命令 go test
:
$ go test .
ok github.com/onexstack/miniblog/internal/pkg/rid0.008s
(2)查看更详细的执行信息
要查看更详细的执行信息可以执行 go test -v
:
$ go test -v .
=== RUN TestResourceID_String
--- PASS: TestResourceID_String (0.00s)
=== RUN TestResourceID_New
--- PASS: TestResourceID_New (0.00s)
=== RUN FuzzResourceID_New
=== RUN FuzzResourceID_New/seed#0
=== RUN FuzzResourceID_New/seed#1
--- PASS: FuzzResourceID_New (0.00s)--- PASS: FuzzResourceID_New/seed#0 (0.00s)--- PASS: FuzzResourceID_New/seed#1 (0.00s)
PASS
ok github.com/onexstack/fastgo/internal/pkg/rid 0.007s
(3)执行测试 N 次
如果要执行测试 N
次可以使用 -count N
命令行选项:
$ go test -v -count 2
=== RUN TestResourceID_String
--- PASS: TestResourceID_String (0.00s)
=== RUN TestResourceID_New
--- PASS: TestResourceID_New (0.00s)
=== RUN TestResourceID_String
--- PASS: TestResourceID_String (0.00s)
=== RUN TestResourceID_New
--- PASS: TestResourceID_New (0.00s)
=== RUN FuzzResourceID_New
=== RUN FuzzResourceID_New/seed#0
=== RUN FuzzResourceID_New/seed#1
--- PASS: FuzzResourceID_New (0.00s)--- PASS: FuzzResourceID_New/seed#0 (0.00s)--- PASS: FuzzResourceID_New/seed#1 (0.00s)
=== RUN FuzzResourceID_New
=== RUN FuzzResourceID_New/seed#0
=== RUN FuzzResourceID_New/seed#1
--- PASS: FuzzResourceID_New (0.00s)--- PASS: FuzzResourceID_New/seed#0 (0.00s)--- PASS: FuzzResourceID_New/seed#1 (0.00s)
PASS
ok github.com/onexstack/fastgo/internal/pkg/rid 0.007s
通过上述测试输出可知,每个测试用例被执行了 2
次。
(4)只运行指定的单测用例
此外,你还可以通过指定 -run
参数(-run
参数支持正则表达式)只运行指定的单测用例:
$ go test -v -run TestResourceID_String
=== RUN TestResourceID_String
--- PASS: TestResourceID_String (0.00s)
PASS
ok github.com/onexstack/fastgo/internal/pkg/rid 0.007s
性能测试
性能测试也叫基准测试,是 Go 项目开发中,非常核心的测试用例类型。Go 开发者也需要掌握如何编写性能测试用例。
在 internal/pkg/rid/rid_test.go 文件中,新增 BenchmarkResourceID_New 性能测试用例函数,代码如下:
func BenchmarkResourceID_New(b *testing.B) {// 性能测试b.ResetTimer() for i := 0; i < b.N; i++ {userID := rid.UserID_ = userID.New(uint64(i))}
}
上述代码定义了一个基准测试函数,用于测量 userID.New
方法的性能表现。函数通过 b.ResetTimer()
重置计时器,确保计时只统计核心测试代码的执行时间,然后在一个循环中根据 b.N
的值多次调用 userID.New(uint64(i))
方法,以模拟高频调用场景并评估其性能。
性能测试函数的名称必须以 Benchmark
开头,例如 BenchmarkXxx
或 Benchmark_Xxx
。默认情况下,go test
不会执行性能测试函数,需通过指定参数 -test.bench
来运行,-test.bench
后需接正则表达式,例如 go test -test.bench=".*"
表示运行所有性能测试函数。在性能测试中,应在循环体中使用 testing.B.N
来多次循环执行测试代码。
在编写性能测试用例时,如果用例需要进行一些耗时的准备工作以测试目标函数,可以在准备工作完成后调用 b.ResetTimer()
方法重置计时器。
实现性能测试用例后,可以执行 go test
命令来运行性能测试用例。在 internal/pkg/rid 目录下,执行 go test -test.bench=".*"
命令来运行性能测试用例:
$ go test -test.bench=".*"
goos: linux
goarch: amd64
pkg: github.com/onexstack/fastgo/internal/pkg/rid
cpu: Intel(R) Xeon(R) Platinum 8260 CPU @ 2.40GHz
BenchmarkResourceID_New-32 180157 6558 ns/op
PASS
ok github.com/onexstack/fastgo/internal/pkg/rid 1.262s
上述测试用例执行结果显示,BenchmarkResourceID_New
用例执行了 180157
次,每次执行的平均时间是 6558
纳秒。1.262s
表示测试用例总的执行时间。
在运行性能测试用例时,还可以通过 -benchtime
命令行选项,来指定性能测试用例的运行时间和运行次数,确保性能测试结果更加稳定,Go 会根据指定的运行时间和运行次数动态调整运行次数(b.N
),以确保测试运行的总时长接近设定值。二者的指定方式如下:
-benchtime=1x
:指定运行次数为1
次(可改为任意次数,例如-benchtime=10x
表示运行10
次);-benchtime=5s
:指定基准测试运行时间为5
秒(可改为其他时间,例如-benchtime=100ms
表示运行100
毫秒)。
运行以下命令,并分别指定性能测试用例的运行时间为 30s
、运行次数为 100000
次:
$ go test -benchtime=30s -test.bench="^BenchmarkResourceID_New$"
goos: linux
goarch: amd64
pkg: github.com/onexstack/fastgo/internal/pkg/rid
cpu: Intel(R) Xeon(R) Platinum 8260 CPU @ 2.40GHz
BenchmarkResourceID_New-32 5459558 6700 ns/op
PASS
ok github.com/onexstack/fastgo/internal/pkg/rid 43.255s
$ go test -benchtime=100000x -test.bench="^BenchmarkResourceID_New$"
goos: linux
goarch: amd64
pkg: github.com/onexstack/fastgo/internal/pkg/rid
cpu: Intel(R) Xeon(R) Platinum 8260 CPU @ 2.40GHz
BenchmarkResourceID_New-32 100000 6507 ns/op
PASS
ok github.com/onexstack/fastgo/internal/pkg/rid 0.661s
在实际运行性能测试用例时,通常会指定运行时间而非运行次数。
测试覆盖率分析
在编写单元测试时,应尽量考虑全面,覆盖所有可能的测试用例,但有时仍可能遗漏一些测试用例。Go 提供了 cover 工具用于统计测试覆盖率。测试覆盖率可以通过以下两条命令完成:
go test -coverprofile=cover.out
:在测试文件目录下运行测试并统计测试覆盖率;go tool cover -func=cover.out
:分析覆盖率文件,用于检查哪些函数未被测试,或者哪些函数内部的分支未完全覆盖。cover 工具通过执行代码的行数与总行数的比例来表示覆盖率。
进入 internal/pkg/rid 目录,执行以下命令,来测试单元测试覆盖率:
$ cd internal/pkg/rid
$ go test -coverprofile=cover.out
$ go tool cover -func=cover.out
github.com/onexstack/fastgo/internal/pkg/rid/rid.go:25: String 100.0%
github.com/onexstack/fastgo/internal/pkg/rid/rid.go:30: New 100.0%
github.com/onexstack/fastgo/internal/pkg/rid/salt.go:18: Salt 100.0%
github.com/onexstack/fastgo/internal/pkg/rid/salt.go:29: ReadMachineID 72.7%
github.com/onexstack/fastgo/internal/pkg/rid/salt.go:50: readPlatformMachineID 75.0%
total: (statements) 81.8%
可以看到 github.com/onexstack/fastgo/internal/pkg/rid 包的单元测试覆盖率为 81.8%
。