Go单元测试有效需跨三硬门槛:文件名_test.go、函数名TestXxx、参数*testing.T;并满足接口抽象与依赖注入两大前提,否则覆盖率虚高、线上仍panic。

go test 能跑起来,不代表你的单元测试真正有效——很多团队的测试覆盖率数字虚高,但线上照样 panic,问题就出在没抓住 Go 单元测试的三个硬门槛和两个设计前提。
Test 函数必须严格满足签名和命名规则
漏掉任意一条,go test 静默跳过,不报错、不提示、不执行,这是新手卡住最久的地方。
- 文件名必须是
_test.go结尾,比如user_service_test.go;test_user.go或user_test.got都无效 - 函数名必须是
TestXxx格式:首字母大写的Test+ 大写开头的后缀,testLogin、Test_login、TESTLOGIN全部被忽略 - 参数类型必须是
*testing.T,写成testing.T(少指针)或*testing.B(性能测试)会导致编译失败或跳过 - 测试函数必须和被测代码在同一个包里,否则无法调用未导出方法或访问包级变量
接口抽象 + 依赖注入是可测性的前提
不抽接口、不传依赖,就只能连真实数据库或 HTTP 服务跑测试——慢、不稳定、不可控,还根本不算单元测试。
- 把外部依赖(如 DB、HTTP Client、缓存)定义为接口,例如:
type UserRepository interface { GetUser(id int) (*User, error) } - 业务逻辑接收该接口作为参数,而不是在函数内部直接 new 具体实现:
func (s *UserService) Login(repo UserRepository, username string) error - 测试时用简单
fake实现,而非第三方 mock 工具:一个只实现必要方法的 struct,返回预设数据或错误即可 - 避免在 fake 中加逻辑(如“根据 ID 返回不同用户”),静态返回值更稳定;动态计算容易让测试随实现漂移
用 go tool cover -func 找出真实盲区
go test -cover 显示 85% 覆盖率,不代表安全。真正危险的是那些从未执行过的分支:error 处理、else 块、defer 清理、context 取消路径。
- 先生成 profile:
go test -coverprofile=coverage.out - 再精准定位:
go tool cover -func=coverage.out,重点关注0.0%、33.3%这类数值 - 看到某函数 coverage 是 0.0%,大概率是因为所有测试都走 happy path,
if err != nil分支完全没触发 - 对每个可能出错的依赖调用,至少构造两个测试 case:一个返回正常值,一个返回
errors.New("timeout")或nil(如 DB 查询无结果) - 测试 context 取消路径时,别用
time.Sleep等超时,而是用ctx, cancel := context.WithCancel(context.Background()); cancel()主动触发
表驱动测试要防闭包陷阱和信息缺失
t.Run 是组织多场景测试的标准方式,但写错细节会让所有子测试行为一致,或失败时无法定位问题。
立即学习“go语言免费学习笔记(深入)”;
- 循环中必须显式拷贝循环变量:
for _, tt := range tests { tt := tt; t.Run(tt.name, func(t *testing.T) { ... }) },否则所有子测试共享最后一个tt - 每个
t.Errorf必须带上输入和期望:t.Errorf("Login(%q) = %v, want %v", tt.username, err, tt.wantErr),不能只写"login failed" - 表格结构体里
name字段要具体,比如"empty_username"、"db_timeout_error",方便go test -run TestLogin/empty_username单独重跑 - 边界值必须显式列出:空字符串、
nil切片、负数 ID、超长 token——这些不是“额外补充”,而是主干测试的一部分
if err != nil 分支、每一个 defer 调用、每一次 context.Done() 检查,都准备一个对应的测试 case。这背后不是技术问题,是设计取舍:你愿不愿意让业务逻辑保持可插拔、可替换、可预测。


















