Go 从 0 到精通 · 第 31 课:测试基础
Go 从 0 到精通 · 第 31 课:测试基础
学习定位:这是整套 Go 教程的第 31 课,也是阶段五(并发与工程阶段)的第七课。
前置要求:已经完成第 30 课,掌握了常见并发设计模式。需要熟悉函数、结构体、错误处理和包管理。
本课目标:掌握 Go 的testing包,理解单元测试的写法和命名约定,学会表驱动测试这一 Go 最经典的测试范式,熟悉go test命令的常用参数,能为自己写的代码编写可靠的测试。
1. 本课你要解决的核心问题
你已经写了 30 课的代码了。每次写完一个函数,你怎么确认它是对的?大概率是 fmt.Println 打印一下看看结果对不对。
这有几个问题:
- 每次改完代码,你得手动再运行一遍看输出
- 多个函数的验证代码混在
main里,越来越乱 - 别人拿到你的代码,不知道怎么验证它是否正确
- 三个月后你自己改了一个函数,不确定有没有影响其他地方
测试就是把"手动验证"变成"自动验证"。 你写一段代码来检查另一段代码的行为,运行一条命令就能知道所有功能是否正常。
Go 在这方面做得非常好:
- 测试是内置的:不需要安装任何第三方框架
- 命令统一:一条
go test就跑所有测试 - 约定简单:文件名以
_test.go结尾,函数名以Test开头 - 表驱动测试:Go 社区的标准测试模式,清晰且可扩展
这一课讲清楚以下问题:
- Go 的测试文件和测试函数长什么样
- 怎么用
t.Error、t.Fatal、t.Log报告结果 - 什么是表驱动测试,为什么 Go 程序员都用它
go test有哪些常用参数- 测试覆盖率怎么看
- 测试辅助函数怎么写
2. 最简单的测试
2.1 先写一个待测函数
假设你有一个文件 math.go:
1 | package mathutil |
2.2 写测试文件
在同一个目录下创建 math_test.go:
1 | package mathutil |
2.3 运行测试
1 | go test |
输出:
1 | PASS |
如果某个测试失败了:
1 | --- FAIL: TestAdd (0.00s) |
2.4 三条铁规
Go 测试有三条约定,必须遵守:
| 约定 | 规则 | 示例 |
|---|---|---|
| 文件名 | 以 _test.go 结尾 |
math_test.go |
| 函数名 | 以 Test 开头,后面跟大写字母 |
TestAdd、TestAbs |
| 参数 | 只有一个 *testing.T 参数 |
func TestAdd(t *testing.T) |
不符合这三条的不会被 go test 识别。
注意:Test 后面必须跟大写字母或下划线。Testadd 不行(小写 a),TestAdd 可以,Test_add 也可以。
3. 报告测试结果的方法
testing.T 提供了几种报告结果的方法,理解它们的区别很重要。
3.1 四个关键方法
1 | func TestExample(t *testing.T) { |
3.2 什么时候用哪个
| 方法 | 效果 | 使用场景 |
|---|---|---|
t.Error |
标记失败,继续执行 | 一个测试函数里检查多个条件 |
t.Fatal |
标记失败,立即停止 | 后续断言依赖当前结果(比如检查返回值前先检查 err == nil) |
t.Log |
记录信息(仅 -v 模式显示) |
调试时记录中间值 |
t.Skip |
跳过当前测试 | 某些环境下不适合运行 |
3.3 典型的 Error vs Fatal 选择
1 | func TestUserService(t *testing.T) { |
4. 表驱动测试
4.1 为什么需要表驱动测试
假设你要测试 Abs 函数。你需要覆盖这些情况:
- 正数 → 返回自身
- 负数 → 返回相反数
- 零 → 返回零
最直接的写法:
1 | func TestAbs(t *testing.T) { |
三个 case 还行,如果有 20 个呢?全是重复代码。
4.2 表驱动测试的标准写法
1 | func TestAbs(t *testing.T) { |
运行 go test -v 的输出:
1 | === RUN TestAbs |
4.3 表驱动测试的结构拆解
1 | // 第一步:定义测试用例的结构 |
4.4 为什么用 t.Run
t.Run 创建子测试。好处:
- 每个 case 有名字:失败时直接告诉你是哪个 case 挂了
- 可以单独运行某个 case:
go test -run TestAbs/负数 - 子测试之间互不影响:一个 case 失败不影响其他 case 继续运行
- 支持并行:
t.Parallel()让子测试并行执行
4.5 更复杂的表驱动测试:带错误检查
1 | package mathutil |
测试:
1 | func TestDivide(t *testing.T) { |
4.6 表驱动测试的核心优势
| 优势 | 说明 |
|---|---|
| 易扩展 | 新增 case 只需要加一行 |
| 结构清晰 | 输入、期望输出一目了然 |
| 减少重复 | 验证逻辑只写一次 |
| 定位方便 | 子测试名称直接指出哪个 case 失败 |
| 可维护 | 修改验证逻辑时只改一处 |
5. go test 命令详解
5.1 常用参数
1 | # 运行当前包的所有测试 |
5.2 常见用法组合
1 | # 开发时最常用:详细输出 + 指定测试 |
5.3 测试缓存
Go 会缓存测试结果。如果代码没变,再次运行 go test 会直接用缓存的结果(输出带 (cached) 标记):
1 | ok mathutil (cached) |
想强制重新运行:
1 | go test -count=1 # 最常用的方式 |
6. 测试覆盖率
6.1 什么是覆盖率
测试覆盖率告诉你:你的测试跑过了代码的百分之多少。
1 | go test -cover |
输出:
1 | PASS |
表示你的测试执行了被测代码中 85.7% 的语句。
6.2 生成覆盖率报告
1 | # 步骤 1:生成覆盖率数据文件 |
go tool cover -func 的输出:
1 | mathutil/math.go:4: Add 100.0% |
6.3 覆盖率的正确态度
- 100% 覆盖率不是目标:追求 100% 往往导致写出大量无意义的测试
- 关注关键逻辑的覆盖:错误处理分支、边界条件、核心业务逻辑
- 覆盖率低不一定坏:某些代码(如
main函数、UI 层)不适合单元测试 - 覆盖率高不一定好:测试可能只是跑过了代码但没有正确断言
实用标准:核心业务逻辑 80%+,工具函数 90%+,整体项目 60%+ 就不错。
7. 测试辅助函数
7.1 t.Helper()
当你提取公共的检查逻辑时,用 t.Helper() 标记辅助函数:
1 | // 辅助函数:检查两个整数是否相等 |
不加 t.Helper(),失败时错误信息指向 assertEqual 函数内部——你还得猜是哪个调用挂了。加了之后,错误信息指向调用 assertEqual 的那一行,定位更快。
7.2 t.Cleanup()
注册清理函数,测试结束后自动执行(类似 defer,但专门为测试设计):
1 | func TestWithTempFile(t *testing.T) { |
7.3 t.TempDir()
Go 1.15+ 提供了更简单的临时目录方法:
1 | func TestWithTempDir(t *testing.T) { |
8. 子测试与并行测试
8.1 子测试的嵌套
1 | func TestMathOperations(t *testing.T) { |
运行指定层级:
1 | go test -v -run TestMathOperations/加法/正数相加 |
8.2 并行测试
对于互不依赖的测试,可以用 t.Parallel() 并行执行:
1 | func TestParallel(t *testing.T) { |
注意:使用 t.Parallel() 时,循环变量需要在 Go 1.22 之前手动捕获。Go 1.22+ 修复了循环变量的问题,不再需要手动捕获。
1 | // Go 1.21 及之前:需要手动捕获 |
9. 完整实战示例
9.1 被测代码:字符串工具包
1 | // stringutil/stringutil.go |
9.2 测试代码
1 | // stringutil/stringutil_test.go |
9.3 运行效果
1 | $ go test -v |
10. 常见坑总结
10.1 文件名或函数名不对
1 | // 坏:文件名不以 _test.go 结尾 |
10.2 测试函数中用了 fmt.Println
1 | // 坏:用 fmt.Println 输出调试信息 |
10.3 表驱动测试中没有给 case 命名
1 | // 坏:没有名字,失败时不知道是哪个 case |
10.4 该用 Fatal 的地方用了 Error
1 | // 坏:err 不为 nil 时继续用 result 会 panic |
10.5 测试之间有依赖
1 | // 坏:Test2 依赖 Test1 创建的全局状态 |
每个测试函数应该是独立的。Go 不保证测试的执行顺序。
11. 本课练习
练习 1:写一个计算器的测试
实现一个 Calculator 包,包含 Add、Subtract、Multiply、Divide 四个函数。为每个函数写表驱动测试,Divide 要测试除以零的错误情况。
练习 2:测试一个栈
实现一个简单的整数栈(Push、Pop、Peek、IsEmpty、Size),为所有方法写测试。Pop 和 Peek 在栈为空时应返回错误,测试要覆盖这些边界情况。
练习 3:测试字符串验证器
实现以下验证函数并写测试:
IsEmail(s string) bool:简单判断是否包含@和.IsPhoneNumber(s string) bool:判断是否是 11 位数字IsStrongPassword(s string) bool:至少 8 位,包含大写、小写和数字
每个函数至少写 6 个测试用例,覆盖正常、异常和边界情况。
练习 4:测试覆盖率
对练习 1 的代码运行覆盖率分析:
- 用
go test -cover查看覆盖率百分比 - 用
go test -coverprofile=coverage.out生成报告 - 用
go tool cover -func=coverage.out查看每个函数的覆盖率 - 如果覆盖率低于 90%,补充测试用例
练习 5:重构已有代码的测试
回到第 23 课的排序代码,为自定义排序逻辑写表驱动测试。要求:
- 测试按年龄排序
- 测试按名字排序
- 测试空切片
- 测试只有一个元素的切片
12. 自测题
12.1 概念题
- Go 测试文件和测试函数的命名规则是什么?
t.Error和t.Fatal的区别是什么?什么时候用哪个?- 什么是表驱动测试?它比逐个断言的写法好在哪里?
t.Run创建的子测试有什么好处?t.Helper()的作用是什么?不加会怎样?go test -cover和go test -coverprofile有什么区别?- 怎么只运行某个特定的测试函数或子测试?
- 为什么测试之间不应该有依赖关系?
12.2 代码阅读题
以下测试代码有两个问题,找出来:
1 | func TestProcess(t *testing.T) { |
点击查看答案
问题 1:没有用 t.Run 和测试名称。当某个 case 失败时,不容易快速定位是哪个 case 的问题。应该给每个测试用例加 name 字段并用 t.Run。
问题 2:err 检查用了 t.Errorf 而不是 t.Fatalf。如果 Process 返回了错误,got 可能是零值,后面的 got != tt.want 断言会给出误导性的错误信息。应该用 t.Fatalf 在出错时立即停止当前子测试。
修复后:
1 | func TestProcess(t *testing.T) { |
13. 本课总结
这一课你学到了 Go 测试的核心内容。
| 知识点 | 要点 |
|---|---|
| 测试约定 | 文件 _test.go,函数 TestXxx(t *testing.T) |
| 报告结果 | t.Error(继续)、t.Fatal(停止)、t.Log(记录) |
| 表驱动测试 | 定义结构体切片 + t.Run 遍历,Go 测试的标准范式 |
| go test | -v(详细)、-run(过滤)、-cover(覆盖率)、-count(禁缓存) |
| 覆盖率 | -coverprofile 生成报告,go tool cover 可视化 |
| 辅助工具 | t.Helper()、t.Cleanup()、t.TempDir()、t.Parallel() |
最重要的三件事:
- 表驱动测试是 Go 的标准测试范式——每个 case 有名字、有输入、有期望输出,用
t.Run遍历 t.Fatal用于前置条件检查(如 err != nil),t.Error用于多个独立断言- 每个测试必须独立——不依赖其他测试的执行结果或顺序
14. 下一课预告
你已经会写基本测试了。下一课我们来学怎么衡量代码的性能。
下一课:Benchmark 与性能测试
会重点讲:
- 基准测试的写法:
BenchmarkXxx(b *testing.B) b.N是什么,Go 怎么自动调整迭代次数- 怎么用
go test -bench运行基准测试 - 怎么对比两个实现的性能差异
- 基准测试的常见误区
学完下一课,你就能用数据说话——“这个实现比那个快多少”,而不是凭感觉猜。





