Go单元测试需严格遵循命名(Test开头)、签名(*testing.T)、文件名(_test.go)、同目录、同包等规则,否则静默失效;t.Run可避免闭包变量共享;依赖隔离优先用接口/函数类型注入而非mock工具。

Go 单元测试不是“配个框架就能跑”,而是靠 go test 和 testing 包原生驱动,但只要漏掉一个命名或签名细节,测试就彻底静默失效——不是报错,是压根不执行。
Test 函数名和签名必须严格匹配
Go 只识别函数名以 Test 开头、且参数为 *testing.T 的函数。大小写敏感,testAdd、TESTAdd、CheckAdd 全部无效。
-
TestAdd(t *testing.T)✅ 正确 -
testAdd(t *testing.T)❌ 不识别 -
TestAdd(t *testing.B)❌ 参数类型错,运行时报 missing argument -
TestAdd(t *testing.T) error❌ 有返回值,编译失败
测试函数必须与被测代码同包(不能在 main 包里测其他包的私有函数),且不能放在单独的 test/ 目录下——否则包路径断裂,无法直接调用。
_test.go 文件名和存放位置不能错
文件名必须以 _test.go 结尾,且与被测源文件同目录。例如 calc.go 对应 calc_test.go,而不是 test_calc.go 或 utils_test.go(除非它真对应 utils.go)。
立即学习“go语言免费学习笔记(深入)”;
- 文件名不含
_test.go→go test完全忽略 - 文件放在
test/子目录 → 包名变成test,无法访问原包私有符号 - 测试文件在模块根目录但无
.go文件 → 提示no Go files in
所有 _test.go 文件不会参与生产构建,零侵入业务代码。
t.Run 是避免闭包陷阱的关键
用循环批量跑测试用例时,不加 t.Run 会导致所有子测试共享最后一次迭代的变量值——这是最隐蔽也最常踩的坑。
错误写法:
for _, tt := range tests {
if got := Add(tt.a, tt.b); got != tt.want {
t.Errorf("...") // 所有 t.Errorf 都引用同一个 tt
}
}
正确写法(必须显式复制变量):
for _, tt := range tests {
tt := tt // 关键:创建新作用域
t.Run(tt.name, func(t *testing.T) {
if got := Add(tt.a, tt.b); got != tt.want {
t.Errorf("Add(%d,%d) = %d, want %d", tt.a, tt.b, got, tt.want)
}
})
}
-
t.Run不只用于分组命名,它强制创建独立作用域 - 漏掉
tt := tt这一行,表驱动测试大概率全部通过或全部失败,结果不可信
依赖隔离优先用字段注入或函数类型
别一上来就找 gomock 或 testify/mock。Go 原生推荐更轻量的方式:把依赖抽象成接口或函数类型,测试时直接替换。
- HTTP 客户端:定义字段
HTTPClient HTTPDoer,测试时传&http.Client{Transport: &mockTransport{}} - 数据库操作:接收
func(context.Context, string, ...any) (sql.Result, error)类型参数,测试时传闭包返回预设值 - 避免在 fake 实现里写分支逻辑——它只是桩,不是简化版业务
真实服务调用会让测试变慢、不稳定、难并行;而 mock 工具引入的行为差异,反而可能掩盖真实集成问题。接口抽象 + 显式注入,才是 Go 测试的惯用路径。


















