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 上稳定通过、不因环境或顺序失败。从第一行测试代码开始,就要把“隔离”和“可重现”当作默认约束,而不是等出了问题再补救。










