go单元测试需严格遵循命名规范:测试文件名必须为*_test.go,函数名必须为TestXxx形式(Xxx首字母大写);推荐使用t.Run组织表驱动测试,避免全局状态和外部依赖以确保稳定可靠。

Go 的单元测试不需要额外框架,go test 命令原生支持,但必须遵守命名和结构约定,否则测试文件不会被识别、函数不会被运行。
测试文件名和函数签名必须严格匹配 *_test.go 和 TestXxx
Go 只扫描以 _test.go 结尾的文件,并只执行函数名符合 Test 开头 + 首字母大写的函数(如 TestAdd)。小写开头(testAdd)、下划线后非大写(Test_add)、或没加 Test 前缀(AddTest)都会被忽略。
- 正确示例:
calculator_test.go中定义func TestAdd(t *testing.T) - 错误示例:
test_calculator.go、func testAdd(t *testing.T)、func Testadd(t *testing.T) - 注意:测试文件应与被测包同目录,除非是
example_test.go这类特殊用途
t.Run 是组织子测试的唯一可靠方式,不用它容易漏测或误判
单个测试函数里混写多个逻辑断言(比如连续调用 t.Errorf),一旦前面失败,后续逻辑仍会执行,但错误堆栈不清晰;而用 t.Run 可隔离场景、支持并行(t.Parallel())、且失败时能精准定位到子测试名。
- 推荐写法:
t.Run("positive numbers", func(t *testing.T) { ... }) - 避免写法:在同一个
TestAdd里硬编码三组输入然后逐个if !equal { t.Error(...) } - 子测试名不要含空格或特殊字符,否则
go test -run过滤可能出错,例如t.Run("1+2=3", ...)不如用"one_plus_two"
表驱动测试(table-driven tests)是 Go 单元测试的事实标准
把输入、期望输出、描述封装成结构体切片,配合 t.Run 循环执行,代码简洁、易扩展、diff 友好。相比每个 case 写一个函数,维护成本低得多。
立即学习“go语言免费学习笔记(深入)”;
func TestParseURL(t *testing.T) { tests := []struct { name string input string wantHost string wantErr bool }{ {"empty", "", "", true}, {"valid", "https://golang.org", "golang.org", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { u, err := url.Parse(tt.input) if (err != nil) != tt.wantErr { t.Fatalf("Parse() error = %v, wantErr %v", err, tt.wantErr) } if !tt.wantErr && u.Host != tt.wantHost { t.Errorf("Parse().Host = %v, want %v", u.Host, tt.wantHost) } }) } }
- 字段名建议统一用
name/input/wantXxx/wantErr,团队协作时可读性高 - 避免在表中调用函数或构造复杂对象——这会让测试数据失去“静态可读性”,也增加调试难度
- 如果某个 case 需要 setup/teardown,把它移到子测试内部,而不是塞进表结构里
慎用 init、全局变量和外部依赖,否则测试不可靠
测试进程是独立启动的,但若包里有 init() 函数修改了全局状态(比如初始化一个共享的 http.Client 或重置了日志级别),不同测试之间可能互相污染;更严重的是,引入真实 HTTP 调用、数据库连接或时间依赖(time.Now()),会导致测试慢、不稳定、无法离线运行。
- 解决方法:把可变依赖抽象为接口参数,测试时传入 mock 实现(如
ClientDoer接口) - 时间相关逻辑:接收
time.Time或func() time.Time作为参数,测试时固定返回值 - 绝对不要在测试中写
os.Setenv后不恢复——用defer os.Unsetenv或临时 map 保存再还原
真正难的不是写第一个 TestXxx,而是让所有测试在 CI 上稳定通过、不因环境或顺序失败。从第一行测试代码开始,就要把“隔离”和“可重现”当作默认约束,而不是等出了问题再补救。