Go单元测试无需额外框架,仅需go test命令和testing包,但必须严格遵守命名(_test.go、TestXxx)、签名(func(t *testing.T))等约定,否则测试不被识别;t.Run可避免闭包变量捕获错误,接口应通过字段注入或函数类型隔离依赖,-race和-cover需纳入日常验证。

Go 的单元测试不需要额外框架,go test 命令和 testing 包原生支持,但必须遵守命名、目录、函数签名等硬性约定,否则测试根本不会被发现或执行。
测试文件和函数名必须严格匹配 *_test.go 和 Test*
Go 只识别以 _test.go 结尾的文件,并且只运行函数名以 Test 开头、参数为 *testing.T 的函数。大小写敏感,testFoo 或 TESTFoo 都无效。
常见错误:
- 把测试写在
main.go里 ——go test完全忽略 - 文件叫
test_utils.go—— 不符合*_test.go模式,不被加载 - 函数叫
CheckAdd或testAdd—— 不以大写Test开头,不被识别
正确示例:calc_test.go 中定义 func TestAdd(t *testing.T)。
立即学习“go语言免费学习笔记(深入)”;
t.Run 是组织子测试、避免变量捕获错误的关键
多个测试用例共用循环变量时,不加 t.Run 会导致所有子测试实际运行最后一次迭代的值 —— 这是 Go 新手最常踩的坑。
使用 t.Run 不仅能分组、命名用例,还创建独立作用域,避免闭包陷阱:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
func TestParseURL(t *testing.T) {
tests := []struct{
name, input string
wantErr bool
}{
{"empty", "", true},
{"http", "http://a.b", false},
{"https", "https://x.y", false},
}
for _, tt := range tests {
tt := tt // 必须显式复制,否则所有 goroutine 共享同一份 tt
t.Run(tt.name, func(t *testing.T) {
_, err := url.Parse(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("Parse(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
}
})
}
}
接口模拟优先用函数类型或结构体字段注入,少用第三方 mock 库
Go 标准库鼓励组合与小接口,大多数依赖可通过函数类型(如 func(string) error)或可替换的结构体字段(如 HTTPClient *http.Client)来隔离,无需复杂 mock。
例如测试一个发 HTTP 请求的函数:
- 错误做法:为
http.Get写 mock,引入gomock或testify/mock - 推荐做法:把客户端抽成字段,测试时传入
&http.Client{Transport: &http.Transport{...}}或更轻量的函数闭包
这样既保持测试快速,又避免 mock 库带来的维护负担和行为偏差。
go test -race 和 go test -cover 要纳入日常验证
竞态检测(-race)不是可选项 —— 只要测试涉及 goroutine、共享变量或 channel,就必须开它,否则极难复现的 data race 会潜伏上线。
覆盖率(-cover)本身不保证质量,但能暴露明显遗漏的分支,比如 error path、边界条件(空输入、超长字符串、负数)是否真有对应测试。
注意:go test -cover 默认只统计被 go test 加载的包,如果测试文件里 import 了其他包但没调用其代码,那些包不会计入覆盖率 —— 别被虚高数字误导。
真正容易被忽略的是:测试中调用的外部服务(数据库、HTTP API)必须被真实隔离,哪怕只是用 httptest.Server 或内存 map 模拟 —— 否则测试就不是单元测试,而是集成测试,稳定性、速度、可重复性全会崩。

















