go单元测试强调约定优于配置:测试文件须以_test.go结尾、函数名以test开头并接收*testing.t;推荐t.run实现表格驱动子测试,对接口而非实现验证,慎用testify。

Go 的单元测试不需要框架加持,testing 包原生支持、结构清晰、执行快——但“优雅”不来自语法糖,而来自测试组织方式、接口抽象程度和对 testify 等辅助库的克制使用。
用 go test 默认约定组织测试文件和函数名
Go 要求测试文件必须以 _test.go 结尾,测试函数必须以 Test 开头且接受 *testing.T 参数。这不是可选项,是强制约定,违反会导致 go test 完全忽略该函数。
常见错误:把测试逻辑写在普通函数里(比如 checkResult()),或把测试文件命名为 utils_test.go.bak —— 后缀不对,go test 就不认。
- 测试文件名应为
xxx_test.go,与被测文件xxx.go同目录 - 测试函数命名推荐
Test_,例如TestParseURL_ValidInput、TestParseURL_Malformed - 避免
TestMain除非真要控制整个包的初始化/清理;多数情况用t.Cleanup()更轻量
用 t.Run() 拆分子测试并隔离状态
单个测试函数里塞多个断言容易耦合、失败后难定位,也难以复用 setup/teardown 逻辑。t.Run() 是 Go 1.7 引入的子测试机制,它让每个测试用例拥有独立生命周期和错误上下文。
典型误用:在 for 循环里直接调用 t.Errorf(),一旦某个 case 失败,后续 case 不再执行,且报错堆栈不带 case 名称。
func TestCalculate(t *testing.T) {
cases := []struct{
input int
want int
}{
{2, 4},
{3, 9},
{-1, 1},
}
for _, c := range cases {
t.Run(fmt.Sprintf("input_%d", c.input), func(t *testing.T) {
got := Calculate(c.input)
if got != c.want {
t.Errorf("got %d, want %d", got, c.want)
}
})
}
}
- 子测试名尽量可读,避免用索引
case_0这类无意义命名 - 每个子测试内不要共享变量(如全局 map 或 struct 字段),否则可能因并发执行产生竞态(
go test -race会报) - 若需共用 setup,放在
t.Run外部;若需 per-case setup,放在子测试函数体内
对接口而非具体实现做测试,避免过度依赖私有字段
当被测对象是结构体时,很多新手会直接检查其内部字段是否等于预期值,比如 if u.Name != "alice"。这会让测试脆弱——只要结构体字段重命名或加中间层,测试就崩,哪怕行为没变。
真正该测的是“它对外表现是否符合契约”,也就是方法返回值、错误类型、副作用(如是否调用了某 mock 接口)。
- 优先验证方法输出:
got, err := svc.Process(req)→ 断言err == nil和got.ID != "" - 避免反射访问未导出字段;若必须校验内部状态,考虑暴露
DebugString()或State()方法用于测试 - 对依赖(如 DB、HTTP client)用 interface 抽象,并在测试中注入 mock 实现,而不是启动真实服务
慎用 testify,优先用标准库 + 明确错误信息
testify/assert 看似让断言更“简洁”,但它隐藏了失败时的关键上下文:哪一行出错、变量实际值是什么、甚至掩盖了 if err != nil 后忘记 t.Fatal 的问题。
标准库的 t.Errorf / t.Fatalf 虽然多敲几字,但错误堆栈干净、语义明确,且 IDE 能直接跳转到失败行。
- 不用
assert.Equal(t, got, want),改用if !reflect.DeepEqual(got, want) { t.Errorf("got %+v, want %+v", got, want) } - 对 error 判断,用
if !errors.Is(err, expectedErr)而非字符串匹配,避免因 error message 微调导致测试失效 - 只在需要表格驱动测试的复杂比较逻辑时,才引入
testify的require包(防止后续断言继续执行)
最常被忽略的一点:测试里的 time.Sleep(100 * time.Millisecond) 看似稳妥,实则制造 flaky test;要用 time.AfterFunc 或 channel 同步,或者干脆重构被测代码,把时间依赖抽成可注入的接口。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











