手写 fake 更快更稳,适用于内部接口;gomock/testify/mock 仅适用于外部模块或方法超10个、需严格校验调用顺序的场景。

什么时候该手写 fake,而不是用 gomock 或 testify/mock
绝大多数 Go 项目里,**手写 fake 结构体比生成 mock 更快、更稳、更容易调试**。gomock/testify/mock 的价值只在特定场景:你要 mock 的接口来自外部 module(比如 cloud.google.com/go/storage.Client),且它没提供可替换的 interface;或者你的接口方法超过 10 个、变更频繁、需要严格校验调用顺序和参数匹配。
常见错误现象:mockgen 生成一堆代码却只用其中 1 个方法;测试里反复写 mockRepo.EXPECT().Get(...).Return(...),但业务逻辑根本没用到 Get 的第二个参数;改了接口签名后忘记重跑 mockgen,导致编译失败或 panic。
- 手写 fake 适合:自己定义的
Notifier、UserRepository、PaymentService等内部接口 - fake 只需实现你真正调用的方法,不用管其他“理论上存在”的方法
- 字段可导出(如
Called bool、LastTo string),测试中直接断言,不依赖AssertExpectations这类黑盒机制 - 如果要模拟并发行为,嵌入
sync.Mutex就行,不用学 gomock 的Times(3)语法
HTTP 客户端测试别碰 http.DefaultClient,用 httptest.Server 替代
硬编码 http.Get 或直接 new http.Client{} 是 mock 失败的根源——它让测试强依赖网络、第三方服务状态,CI 上随机超时、404、SSL 错误全来了。
正确做法是把请求入口抽成可配置字段,测试时注入 httptest.Server.URL:
立即学习“go语言免费学习笔记(深入)”;
type UserClient struct {
BaseURL string
Client *http.Client
}
<p>func (c <em>UserClient) GetByID(ctx context.Context, id string) (</em>User, error) {
resp, err := c.Client.Get(c.BaseURL + "/users/" + id)
// ...
}- 测试中启动服务器:
server := httptest.NewServer(handler),然后defer server.Close() - 务必传
server.URL给被测 client,不是拼接字符串(避免路径不一致) - handler 中用
json.NewEncoder(w).Encode(...)返回响应,别用 raw string,否则Content-Type缺失导致解析失败 - 若被测代码已固定用
http.DefaultClient且无法改,才考虑httpmock,但它对自定义Transport无效,且不报错——这是最隐蔽的坑
数据库 mock 别绕开 sqlmock,但得会写靠谱的 SQL 匹配
sqlmock 是目前最成熟、最贴近真实 *sql.DB 行为的方案。它不 mock 接口,而是 mock database/sql 底层交互,所以能覆盖事务、预处理语句、多行查询等真实场景。
常见错误现象:mock.ExpectQuery("SELECT * FROM users") 匹配失败,因为实际执行的 SQL 带了换行或空格;WillReturnRows(rows) 传了空 sqlmock.Rows 却忘了调用 AddRow;忘记最后调用 mock.ExpectationsWereMet(),导致 SQL 没执行也不报错。
- 正则匹配更可靠:
mock.ExpectQuery("^SELECT.*FROM users WHERE id = \?$"),注意转义 - 用
WithArgs(123)显式声明参数预期,避免类型不匹配(int vs int64) - 查不到数据?用
WillReturnError(sql.ErrNoRows),别返回nil, nil - 插入失败?用
ExpectExec("^INSERT").WillReturnError(fmt.Errorf("duplicate"))
为什么不要用 monkey patch 或改包级变量做 mock
Go 编译器对包级符号有强绑定,运行时篡改 http.DefaultClient 或函数变量,会导致测试间污染、竞态、panic 堆栈指向工具内部而非业务代码——尤其在 go test -race 下大概率崩溃。
典型表现:TestA 改了 time.Now,TestB 突然因时间戳异常失败;TestC 调用 os.Getenv 返回空,因为 TestD 把它 patch 成了固定值但没恢复。
- Go 的
func类型不是 first-class 可安全替换的对象,尤其涉及方法集时 -
go:linkname在 Go 1.20+ 已被模块校验严格限制,monkey库基本不可用 - 替代方案很简单:把依赖提成结构体字段或函数参数,测试时传入闭包或 fake 实现(例如
NowFunc func() time.Time) - 所有“看起来很酷”的运行时 patch,最终都会变成 CI 上没人敢删的 tech debt
接口抽象 + 依赖注入不是“最佳实践”的说辞,而是 Go 类型系统天然支持的最小可行路径。越早把 http.Client、*sql.DB、第三方 SDK 封装进 interface 并通过字段注入,后续 mock 就越轻量。最难的从来不是写 mock,而是让代码一开始就能被测。


















