8.1 测试技巧:单元测试(Unit Test)
单元测试(Unit Test, UT) 是健康项目里很重要的一环:需求变动多、多人协作时,没有测试兜底,很容易在「改 A 坏 B」时把问题带到线上。
写测试短期多写一点代码,长期却能减少回归 bug、方便重构。不少团队排期紧,测试被挤掉——一旦核心逻辑缺少用例,后面补的成本会更高。
典型痛点包括:
- 同事改了你不熟的函数,漏测异常分支,问题直接上线。
- 函数分支多、改动频繁,每次手工验证场景费时费力。
一、如何写单元测试
1. 准备示例代码
在空目录中初始化模块(模块路径可按项目修改):
go mod init example.com/reverse待测逻辑一般放在独立包里(如 package reverse),与测试文件并列,便于复用与 go test。若项目只是可执行程序,把代码写在 package main 里、同目录放 *_test.go 也同样合法,本章从常见库结构讲起。
新建 reverse.go,只放待测函数(包名与目录习惯一致,便于阅读):
package reverse
func Reverse(s string) string {
b := []byte(s)
for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}2. 测试文件约定
为 Reverse 编写 reverse_test.go,与 reverse.go 同属 package reverse 即可(白盒,可直接测未导出细节);若写成 package reverse_test,则只能测导出 API,相当于黑盒测试,此处从简。
约定简要记:
| 约定 | 说明 |
|---|---|
| 文件名 | *_test.go |
| 函数名 | TestXxx,Xxx 首字母大写 |
| 参数 | 必须是 t *testing.T |
| 包 | 需 import "testing" |
3. 快速入门:第一个测试
先写只测一组数据的测试,把「导入 testing → 调用被测函数 → 与期望比较 → 失败则 t.Errorf」跑通。下面在 reverse_test.go 中编写:
package reverse
import "testing"
func TestReverseHello(t *testing.T) {
in := "Hello, world"
want := "dlrow ,olleH"
got := Reverse(in)
if got != want {
t.Errorf("Reverse(%q) == %q, want %q", in, got, want)
}
}在模块根目录执行 go test / go test -v,应能通过;至此已具备被测实现与一个可执行的测试函数。
4. 核心测试方法
*testing.T 上常用的失败标记、日志与子测试入口如下(均通过参数 t 调用):
| 方法 | 说明 |
|---|---|
t.Error() / t.Errorf() | 标记当前测试失败,继续执行当前测试函数内后续代码。 |
t.Fatal() / t.Fatalf() | 标记失败并立即终止当前测试函数(其后代码不再执行)。 |
t.Log() / t.Logf() | 输出日志;仅在使用 go test -v 时才会出现在测试输出中(无 -v 时默认不打印)。 |
t.Run() | 创建子测试并指定名称,便于分层组织用例及与 -run 的子名匹配;示例见本章第三节。 |
5. 表驱动测试
当多组输入/期望都要测时,把用例放进切片里循环执行,叫作表驱动(table-driven):结构清晰,加新用例只往表里加一行。
与上文「快速入门:第一个测试」相比,核心仍是「调用 Reverse → 比较 → t.Errorf」,此处用 for 遍历多组 in / want。t.Errorf 不会中断整个测试函数,其余用例仍会执行(遇错即停可用 t.Fatalf,见上文「核心测试方法」)。
package reverse
import "testing"
func TestReverse(t *testing.T) {
testcases := []struct {
in, want string
}{
{"Hello, world", "dlrow ,olleH"},
{" ", " "},
{"!12345", "54321!"},
}
for _, tc := range testcases {
rev := Reverse(tc.in)
if rev != tc.want {
t.Errorf("Reverse(%q) == %q, want %q", tc.in, rev, tc.want)
}
}
}同一文件里可同时保留 TestReverseHello 与 TestReverse;若只保留一种写法,删除不再需要的测试函数即可。
二、执行测试用例
在包含 go.mod 的模块根目录(或对应包目录)执行:
1. 默认:go test
不加参数时,运行当前包下所有测试,通过则只打印简要结果:
go testPASS
ok example.com/reverse 0.012s(ok 后为模块导入路径与耗时,以本机为准。)
2. 详细输出:go test -v
-v(verbose)会打印每个测试函数的执行情况(如 RUN / PASS):
go test -v=== RUN TestReverse
--- PASS: TestReverse (0.00s)
PASS
ok example.com/reverse 0.012s若某条断言失败,t.Errorf 会输出差异,且该测试记为失败;可用 -count=1 禁用缓存、反复确认(调试时常用)。
3. 常用命令示例
以下命令均在待测包所在目录(或模块上下文中针对该包的路径)下执行;go test 默认处理当前目录对应的那一个包。
运行当前包内全部测试(逐条输出):
go test -v只运行名称匹配的测试函数(-run 的值为正则;是否加 -v 均可,加 -v 便于查看日志):
go test -v -run TestAdd-run 使用正则匹配完整测试名(默认未锚定,子串即可命中;需精确到某一函数时可写更严的正则)。子测试名形如 TestFoo/bar 时的写法见第三节。
查看测试覆盖率:
go test -cover
go test -coverprofile=coverage.out
go tool cover -html=coverage.out-cover 在终端打印当前包的语句覆盖率摘要;-coverprofile 将覆盖率数据写入指定文件(文件名可自定),再由 go tool cover -html 生成本地 HTML 报告并在浏览器中查看。go tool cover 与 go test 一样随 Go 安装提供。
三、子测试与 -run
1. 按名字过滤:-run
-run 的参数是正则,只运行函数名匹配的测试。例如只跑 TestReverse(与第二节第 3 小节命令形式相同):
go test -v -run TestReverse输出与「跑全部测试」时类似,只是只执行匹配的测试函数。
2. 子测试:t.Run
复杂场景可把一个大测例拆成多个 子测试,每个子测试有自己的名字,便于单独跑、单独看日志:
package reverse
import "testing"
func TestReverse(t *testing.T) {
t.Run("foo", func(t *testing.T) {
testcases := []struct {
in, want string
}{
{"Hello, foo", "oof ,olleH"},
}
for _, tc := range testcases {
rev := Reverse(tc.in)
if rev != tc.want {
t.Errorf("[foo] Reverse(%q) == %q, want %q", tc.in, rev, tc.want)
}
}
})
t.Run("bar", func(t *testing.T) {
testcases := []struct {
in, want string
}{
{"Hello, bar", "rab ,olleH"},
}
for _, tc := range testcases {
rev := Reverse(tc.in)
if rev != tc.want {
t.Errorf("[bar] Reverse(%q) == %q, want %q", tc.in, rev, tc.want)
}
}
})
t.Run("zh_ch", func(t *testing.T) {
testcases := []struct {
in, want string
}{
{"金山集团", "团集山金"},
{"😀😃😄", "😀😃😄"},
}
for _, tc := range testcases {
rev := Reverse(tc.in)
if rev != tc.want {
t.Errorf("[zh_ch] Reverse(%q) == %q, want %q", tc.in, rev, tc.want)
}
}
})
}子测试全名是 主测试名/子名。只跑 foo 子测试可写:
go test -v -run TestReverse/foo典型输出类似:
=== RUN TestReverse
=== RUN TestReverse/foo
--- PASS: TestReverse/foo (0.00s)
--- PASS: TestReverse (0.00s)
PASS
ok example.com/reverse 0.012s只跑 bar:-run TestReverse/bar。需要匹配层级时,正则要能对上完整名字(细节请用 go help testflag 在本地查阅)。
四、性能测试:基准测试(Benchmark)
日常说的「压力测试」在 Go 里常通过 testing 包的基准测试完成:反复调用某段代码,由运行环境统计耗时、分配次数等,用来观察热点、对比优化前后差异。它与单元测试共用 go test,但默认不会跑基准函数,需要加 -bench 参数。
1. 函数格式
- 函数名以
Benchmark开头,且Benchmark后首字母大写(与Test规则类似)。 - 参数为
b *testing.B,循环里写for i := 0; i < b.N; i++:框架会调整b.N,使单次基准运行时间足够稳定。
package reverse
import "testing"
func BenchmarkReverse(b *testing.B) {
s := "Hello, world"
for i := 0; i < b.N; i++ {
Reverse(s)
}
}若基准前有一次性初始化(读文件、建连接等),可用 b.StopTimer() / b.StartTimer() 包住初始化,避免把准备时间算进被测函数。
2. 如何运行
在包目录执行(示例:只跑名字里含 Reverse 的基准):
go test -bench=Reverse跑当前包全部基准可写 -bench=.(. 表示匹配任意)。需要同时看内存分配时可加 -benchmem。
示意输出(数值随机器变化):
goos: darwin
goarch: amd64
pkg: example.com/reverse
BenchmarkReverse-8 50000000 24.1 ns/op
PASS
ok example.com/reverse 0.856s-8 表示 GOMAXPROCS;ns/op 表示每次 b.N 循环里执行一次循环体的平均耗时(纳秒/次)。加 -benchmem 时还会多出 B/op、allocs/op 等列。
五、机制与约定速查
以下条目可与第一节至第四节中的示例对照;完整标志与行为以 go help test、go help testflag 及 testing 包文档为准。
1. go test 与执行流程
go test 随 Go 发行版提供:在包内扫描 *_test.go 中符合约定的函数,生成临时测试入口程序,编译、运行并汇总通过/失败,再清理中间产物。测试源文件使用与普通 Go 代码相同的语法与类型系统。
识别哪些函数参与测试、如何匹配 -run / -bench 等,由工具与 testing 包共同约定;实现细节可能随版本调整,以官方说明为准。
2. *_test.go 与正式发布构建
- 与实现同目录的
*.go/*_test.go便于就近维护测试。 go build、go install默认不将*_test.go编入可执行文件或库产物;这些文件主要在执行go test(及少数相关构建路径)时参与编译。
3. 测试相关函数前缀
同一份 *_test.go 中,以下前缀的函数由 go test 按规则调度(是否运行还依赖是否传入 -bench 等参数):
| 前缀 | 典型签名 | 作用 |
|---|---|---|
Test | func TestXxx(t *testing.T) | 单元测试:断言行为是否符合预期 |
Benchmark | func BenchmarkXxx(b *testing.B) | 基准测试:性能与分配(需 -bench) |
Example | func ExampleXxx(),可选 // Output: | 示例:文档与可运行样例,输出可由 go test 校验 |
Test*、Benchmark* 已在前文示例中出现;Example* 本文未展开。
4. Test* 函数约定(编译与识别)
- 源文件以
_test.go结尾。 - 函数名以
Test开头,且Test后首字符为大写字母(如TestReverse,不能为Testreverse)。 - 签名为
func TestXxx(t *testing.T),文件内import "testing"。 - 在待测包所在目录(或模块上下文中指向该包)执行
go test以运行该包测试。
5. 常用命令行标志
| 标志 | 含义(简述) |
|---|---|
-v | 逐条输出测试的运行与结果(见第二节) |
-run regexp | 仅运行名称匹配正则的测试(见第三节,含 TestFoo/bar 形式) |
-count n | 每个测试执行 n 次;-count=1 常用于绕过测试结果缓存以便调试 |
-cover | 语句覆盖率摘要(-coverprofile、go tool cover 见第二节第 3 小节) |
-bench regexp | 运行基准测试(见第四节);匹配 Benchmark* 名称 |
-benchmem | 与 -bench 联用时可输出分配相关列 |
与 go test 子命令、环境变量、覆盖率输出格式等有关的其余选项,以本地 go help test 为准。