接口抽象是mock的前提,go因无运行时类型擦除,mock必须实现与真实依赖完全一致的接口;需将外部依赖行为抽象为小而专注的接口,仅含实际调用方法,并通过构造函数注入依赖,http测试优先用httptest.server,手写mock更轻量高效。

接口抽象是Mock的前提,不是可选项
Go语言没有运行时类型擦除或鸭子类型,mock对象必须和真实依赖实现**同一个接口**,否则编译直接报错,比如 cannot use *MockDB as *sql.DB 或 cannot use MockSender as EmailSender。这不是测试写错了,是类型系统在提醒你:没提前定义契约。
关键动作只有两步:
- 把外部依赖(数据库、HTTP客户端、邮件服务等)的行为抽象成小而专注的接口,只包含当前函数实际调用的方法
- 让真实实现和Mock结构体都实现该接口,且函数参数、字段类型全部使用该接口,而非具体类型(如
*http.Client、datastore.Storage)
例如,若函数只调用 Get 和 Set,就别定义带 Delete 和 List 的大接口——多余方法会迫使Mock也实现,徒增维护成本。
构造函数注入比全局变量/单例更可控
如果依赖是在结构体内直接 new 出来的(比如 client := &http.Client{}),那它在测试里根本没法替换。Mock只能生效于“可被传入”的位置。
正确做法是把依赖作为参数交给构造函数:
- 业务结构体字段声明为接口类型,如
sender EmailSender,而非*smtp.Client - 提供显式构造函数,如
NewOrderService(sender EmailSender) - 测试时传入
&MockEmailSender{},生产时传入&SMTPSender{client: realClient}
避免使用 init() 或包级变量初始化依赖——它们会锁死行为,Mock无从介入。
HTTP依赖优先用 httptest.Server,不是 httpmock
硬编码调用 http.Get 或直接 new *http.Client 会导致测试强耦合网络状态。最稳妥的方式是把请求入口抽成可配置字段(如 BaseURL string + Client *http.Client),然后在测试中启动 httptest.Server。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
常见陷阱:
- 忘记
defer server.Close()→ 端口泄漏,后续测试失败 - 手动拼接 URL(
server.URL + "/api/user")→ 路由匹配失效,handler 没被触发 - handler 返回 raw string → 缺少
Content-Type: application/json,导致 JSON 解析失败
正确示例中,server.URL 仅用于初始化 client,所有路径匹配和响应格式均由 handler 控制。
手写 Mock 比 gomock/testify/mock 更快更稳
90% 的内部接口(如 UserRepository、PaymentGateway)不需要代码生成工具。手写一个结构体,导出字段(如 Called bool、LastID int),实现对应方法即可。
优势很实在:
- 字段可直接断言:
if !mock.Called { t.Fatal("Send not called") },不用学EXPECT().Send().Times(1) - 不依赖额外构建步骤(
mockgen),改接口后无需重生成 - 并发安全易加:
sync.Mutex嵌入结构体,Lock()/Unlock()控制状态
gomock/testify/mock 真正有用的地方很窄:你要 mock 的是外部 module 的接口(如 cloud.google.com/go/storage.Client),且它没暴露可用接口;或者接口方法超过 10 个、调用顺序必须严格校验——否则就是过度工程。
真正难的不是写 Mock,而是决定哪些依赖必须隔离、哪些可以接受集成测试。HTTP 和数据库调用几乎总是要 Mock;但纯内存计算、字符串处理这类逻辑,直接测更干净。别为了 Mock 而 Mock。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










