大家好,我是极客老墨!
刚转 Go 的时候,我还在找"Go 版的 JUnit 在哪"。结果发现,Go 根本不需要第三方测试框架,go test 命令 + testing 包就够了。
写测试不只是为了完成 KPI,更是为了**“让自己晚上能睡个好觉”**。这篇我们不仅聊基础,更要聊聊在大厂工程实践中,如何写出既稳健又专业的测试。
Go中的测试,不是"一等公民",是"超等公民"。Go 编译器自带 go test 工具,标准库提供 testing 包,写测试简单到爆。
1. 单元测试:从入门到专业
Go 的测试文件规则很简单:以 _test.go 结尾,函数名以 Test 开头。
1.1 基础写法
1func TestAdd(t *testing.T) {
2 if got := Add(1, 2); got != 3 {
3 t.Errorf("Add(1, 2) = %d; want 3", got)
4 }
5}
跑一下:
1$ go test -v
2=== RUN TestAdd
3--- PASS: TestAdd (0.00s)
4PASS
5ok example.com/math 0.392s
就这么简单。不需要装任何库,不需要配置文件。
1.2 进阶:表格驱动(标配)
这是 Go 社区的灵魂写法。把数据和逻辑分离,增加测试用例只需在切片里加一行,这就是“表格驱动”。
1func TestAddTableDriven(t *testing.T) {
2 // 1. 定义测试用例
3 tests := []struct {
4 name string // 用例名称
5 a, b int // 输入
6 want int // 期望结果
7 }{
8 {"正数", 1, 2, 3},
9 {"负数", -1, -2, -3},
10 {"混合", -1, 1, 0},
11 {"零", 0, 0, 0},
12 }
13
14 // 2. 遍历执行
15 for _, tt := range tests {
16 // t.Run 启动子测试,方便定位错误
17 t.Run(tt.name, func(t *testing.T) {
18 got := Add(tt.a, tt.b)
19 if got != tt.want {
20 t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.want)
21 }
22 })
23 }
24}
为什么推荐表格驱动?
- 清晰:数据和逻辑分离,加用例只需加一行
- 隔离:用
t.Run,某个用例失败不影响其他用例
这是 Go 社区的标准写法,看到 Go 开源项目的测试代码,基本都是这个套路。
老墨避坑指南:
老墨避坑指南: 别偷懒只写一个用例。在
go-tutorial-code/12-testing中,我同步了很多高阶示例,包含了异常情况的测试建议,快去看看!
2. 工程高阶:Setup 与 Teardown
有时候测试前需要连数据库,测试后需要清理数据。Go 提供了 TestMain 钩子。
1func TestMain(m *testing.M) {
2 // 1. 全局资源初始化 (Setup)
3 fmt.Println("开始全局初始化...")
4
5 // 2. 运行所有测试
6 code := m.Run()
7
8 // 3. 全局资源释放 (Teardown)
9 fmt.Println("开始清理全局资源...")
10
11 os.Exit(code)
12}
3. 并行与竞态:揪出隐藏的 Bug
3.1 t.Parallel()
如果你的测试互不干扰,可以通过在测试函数开头调用 t.Parallel() 开启并行,极大缩短测试耗时。
3.2 -race 竞态检测(神兵利器)
这是老墨最推荐的功能。运行测试时带上 -race,Go 运行时会实时监控内存访问,帮你揪出那些万恶的并发冲突。
1go test -race ./...
4. 模拟神器:面向接口的 Mock
在企业级开发中,我们经常要测试“依赖数据库”或“依赖外部 API”的逻辑。这时候千万不要真连。
核心思想:定义接口,在测试中传入 Mock 实现。这样你的测试就不再依赖环境,跑得飞快。
1// 业务逻辑
2type DB interface { GetUser(id int) string }
3
4func GetUserInfo(db DB, id int) string {
5 return "User: " + db.GetUser(id)
6}
7
8// 测试中的 Mock 实现
9type MockDB struct{}
10func (m MockDB) GetUser(id int) string { return "老墨" }
11
12func TestGetUserInfo(t *testing.T) {
13 mock := MockDB{}
14 got := GetUserInfo(mock, 1) // 传入 mock
15 if got != "User: 老墨" {
16 t.Errorf("got %s, want User: 老墨", got)
17 }
18}
5. 最佳实践:黑盒与白盒
- 内部测试:包名依然是
package mypkg,可以测试未导出的函数(白盒)。 - 外部测试:包名改为
package mypkg_test,只能从外部视角测试导出函数(黑盒),有效防止“过度依赖内部实现”。这也是 Go 官方最推荐的方式。
6. 基准测试:拿数据说话
基准测试用来测性能(CPU 时间、内存分配)。函数名以 Benchmark 开头,参数是 *testing.B。
不要凭感觉猜哪个算法快,用基准测试跑分:
1func BenchmarkAdd(b *testing.B) {
2 // b.N 是 Go 自动调整的循环次数(可能是 1000、1000000...)
3 // 目的是让运行时间足够长,得到稳定的平均值
4 for i := 0; i < b.N; i++ {
5 Add(1, 2)
6 }
7}
运行:go test -v -bench=. -benchmem。-benchmem 能让你看到内存分配情况,这在优化性能时至关重要。
1# . 表示匹配所有基准测试
2$ go test -bench=.
3goos: darwin
4goarch: arm64
5pkg: example.com/math
6BenchmarkAdd-8 1000000000 0.315 ns/op
7PASS
8ok example.com/math 0.548s
怎么看结果?
BenchmarkAdd-8:8 表示用了 8 个 CPU 核心(GOMAXPROCS)1000000000:循环了 10 亿次0.315 ns/op:平均每次操作 0.315 纳秒(快到飞起)
7. 测试覆盖率:看看测了多少代码
想知道你的测试到底靠不靠谱?
1$ go test -cover
2PASS
3coverage: 100.0% of statements
4ok example.com/math 0.392s
还能生成可视化报告:
1go test -coverprofile=coverage.out
2go tool cover -html=coverage.out
浏览器会打开一个 HTML 页面,绿色是已覆盖,红色是未覆盖,一目了然。
老墨总结
Go 的测试哲学:原生、极简、工程化。
- 表格驱动是入门必修课。
t.Parallel()+-race是并发开发的保障。- 面向接口 Mock 是解耦测试的关键。
TestMain用于管理昂贵的外部资源。
实战建议:
- 写代码的同时写测试,别等到最后
- 表格驱动测试是标配,别偷懒
- 性能敏感的代码一定要跑基准测试
永远不要为了覆盖率而写测试,要为了**“核心业务逻辑的正确性”**而写。
练习题
- 基础题:为你的字符串处理工具写一组表格驱动测试。
- 进阶题:使用
TestMain模拟一个数据库初始化和清理的过程。 - 挑战题:编写一个会发生 Race Condition 的子测试,尝试用
-race捕获并将测试改为并行运行。
你写 Go 测试时用表格驱动吗?有没有遇到过测试覆盖率的坑?欢迎评论区讨论!
极客老墨,继续折腾!
如果有任何问题,欢迎在评论区留言或关注公众号「极客老墨」交流。
完整示例代码在 go-tutorial-code/12-testing。