大家好,我是极客老墨!

刚转 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 的测试哲学:原生、极简、工程化

  1. 表格驱动是入门必修课。
  2. t.Parallel() + -race 是并发开发的保障。
  3. 面向接口 Mock 是解耦测试的关键。
  4. TestMain 用于管理昂贵的外部资源。

实战建议

  • 写代码的同时写测试,别等到最后
  • 表格驱动测试是标配,别偷懒
  • 性能敏感的代码一定要跑基准测试

永远不要为了覆盖率而写测试,要为了**“核心业务逻辑的正确性”**而写。

练习题

  1. 基础题:为你的字符串处理工具写一组表格驱动测试。
  2. 进阶题:使用 TestMain 模拟一个数据库初始化和清理的过程。
  3. 挑战题:编写一个会发生 Race Condition 的子测试,尝试用 -race 捕获并将测试改为并行运行。

你写 Go 测试时用表格驱动吗?有没有遇到过测试覆盖率的坑?欢迎评论区讨论!


极客老墨,继续折腾!

如果有任何问题,欢迎在评论区留言或关注公众号「极客老墨」交流。

完整示例代码在 go-tutorial-code/12-testing


相关阅读