必须用接口定义依赖才能 mock,因为mock本质是提供满足同一契约的替代实现;若业务代码直接依赖*sql.DB等具体类型则无法替换,只有依赖UserRepo等接口时才能注入MockUserRepo。

为什么必须用接口定义依赖才能 mock
Go 里没法对结构体或函数直接 mock,只能对 interface 做——因为 mock 的本质是「提供另一个满足同一契约的实现」。如果你的业务代码直接依赖 *sql.DB 或 StripeClient 这种具体类型,测试时就无法替换;只有它依赖的是 UserRepo 这样的接口,你才能塞进 MockUserRepo。
常见错误现象:cannot use *MockDB (type *MockDB) as type *sql.DB in argument——说明你试图把 mock 当成具体类型传,而不是接口。
- 生产代码中所有外部依赖入口点都应声明为接口字段,例如
db DBInterface而非db *sql.DB - 不要 mock 标准库类型(如
*http.Client),而是先封装成接口再 mock - 接口方法要尽量窄:一个接口只含 2–3 个方法,避免
UserService里混着 DB、HTTP、Cache 逻辑
手动 mock 比自动生成更可控的场景
小项目、核心逻辑简单、接口方法少于 4 个时,手写 mock 结构体比跑 mockgen 更快也更透明。你一眼就能看清它返回什么、是否记录调用、怎么触发 error。
典型写法:
立即学习“go语言免费学习笔记(深入)”;
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
type MockUserRepo struct {
GetUserFunc func(id int) (*User, error)
SaveUserFunc func(*User) error
}
func (m *MockUserRepo) GetUser(id int) (*User, error) {
if m.GetUserFunc != nil {
return m.GetUserFunc(id)
}
return nil, errors.New("not stubbed")
}
- 字段用函数类型(
func())而非map或switch,便于测试中直接赋值闭包 - 不初始化默认行为,强制测试显式设置期望路径,避免“看似通过实则没测”
- 如果需要验证调用次数或参数,加
CallCount int和LastID int字段,不用引入testify/mock
用函数变量打桩(stub)替代接口 mock 的适用场景
当依赖项只是几个独立函数(比如数据库查询、HTTP 调用),且不涉及状态或多次调用验证时,直接用函数变量打桩更轻量、无侵入。
示例:
var GetRecordCountByCardId = func(ctx context.Context, cardNum string, event int) (int64, error) {
return new(models.GiftCardLogs).GetRecordCountByCardId(ctx, cardNum, event)
}
// 测试中重赋值
GetRecordCountByCardId = func(ctx context.Context, cardNum string, event int) (int64, error) {
return 2, nil
}
- 适用于无状态、单点替换的依赖,比如工具函数、第三方 SDK 封装层
- 注意:全局变量打桩需在
Test函数开头重置,避免测试间污染 - 不能验证调用顺序或参数细节,也不适合需要并发安全或多次调用计数的场景
接口太宽导致 Mock 成本飙升
比如定义 type DB interface { Query(); Exec(); Begin(); Commit(); Rollback(); Ping() },结果测试里为了测一个 GetUser,还得实现全部 6 个方法——哪怕其他方法根本不会被调用。
解决办法不是补全所有方法,而是重构接口:
- 按使用方拆分:DAO 层只依赖
UserRepo,事务控制单独抽TxManager - 按职责聚合:不要让一个接口同时承载数据读写、缓存、日志能力
- 优先用组合而非大接口:多个窄接口组合进 struct,比一个胖接口更容易 mock 和复用
真正难的不是写 mock,而是让接口边界清晰到不需要写太多 mock。

















