
为什么 go test 跑不通,不是因为没写测试,而是没写对「可测性」
Go 的测试驱动开发(TDD)成败不取决于你多快写出 TestXxx 函数,而在于代码是否天然支持被隔离、被替换、被断言。很多团队卡在「写了测试但改一行逻辑就全挂」,本质是结构没按依赖倒置组织。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 把外部依赖(数据库、HTTP 客户端、时间)全部抽象成接口,且定义在被调用方包里(比如
user.Service包里定义UserRepo接口),而不是在 infra 包里硬写实现 - 构造函数接收依赖,而非在方法内 new 实例 —— 否则
sql.Open或http.DefaultClient会直接进测试流程 - 避免在
init()里做任何有副作用的操作,它无法被 mock,也无法被重置
用 testify/mock 还是纯 interface + struct 手写 mock?
结论:90% 的场景,手写 mock 更稳、更轻、调试更直白。第三方 mock 工具在 Go 里容易引入反射依赖和生成代码维护成本,尤其当你只 mock 一两个方法时。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 为要 mock 的接口单独写一个
mock_xxx.go文件,放在同包下的xxx_test子目录(如user/mock_userrepo.go),避免污染主逻辑 - 手写 mock 结构体时,用字段控制行为分支,比如
ReturnError bool或ReturnUsers []User,比动态 expect 更易追踪 - 如果真要用
testify/mock,务必配合gomock的-source参数生成,且每次接口变更后重新生成 —— 遗留旧 mock 是常见 panic 来源
table-driven tests 怎么写才不变成“数据堆砌”?
表格驱动测试不是把所有输入输出列出来就完事。真正起效的前提是:每个 case 真正验证一个独立设计意图,比如边界值、错误路径、状态跃迁。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- case 名用描述性字符串,而不是序号:
"returns_error_when_email_is_empty",不是"case1" - 结构体字段命名体现关注点:
input、expectedErr、expectedOutput,别用in/out这种模糊名 - 避免在 table 中调用真实函数(如
time.Now()),否则并发测试可能因时间漂移失败;统一用固定time.Time值或注入clock接口
为什么 TDD 在 Go 里常“半途而废”?关键卡点是重构阶段没动 go:generate 和接口位置
很多人 TDD 走到一半放弃,不是因为写不出测试,而是重构时发现:新加的 interface 放错了包,导致循环导入;或者 go:generate 注释没同步更新,mock 代码失效却没报错。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 接口定义优先放在「使用它」的包里,而非「实现它」的包 —— 这是 Go 社区约定,也是解耦前提
- 所有
go:generate指令必须加//go:build ignore,并在 CI 中显式运行go generate ./...并校验 git diff,否则很容易漏掉 mock 更新 - 重构函数签名后,立刻检查所有调用处是否仍满足接口契约 —— Go 不会报错,但 runtime 可能 panic
最难的从来不是写第一个 TestCreateUser,而是当业务逻辑变复杂时,还能让每个新 case 不破坏已有 mock 行为、不绕过接口抽象、不触发隐式依赖。这点没绷住,TDD 就退化成「测着玩」。











