Go测试中不能直接new真实依赖,因其破坏隔离性、速度和可重复性;应通过接口抽象+依赖注入实现mock,手写轻量mock更推荐。

Go 测试中为什么不能直接 new 一个真实依赖?
因为真实依赖(比如数据库、HTTP 客户端、文件系统)会破坏测试的隔离性、速度和可重复性。调用 sql.Open 可能连接失败,http.Get 可能超时或返回非预期状态,这些都不是单元测试该关心的。
核心原则:测试只验证被测函数的逻辑,不验证第三方服务是否在线或响应是否正确。
所以必须把依赖「替换」成可控的模拟实现——不是靠全局变量赋值或反射修改,而是通过接口抽象 + 依赖注入来实现。
mock 的前提是定义 interface,不是 struct
Go 的 mock 本质是实现同名方法签名的假对象。如果你的生产代码直接依赖 *sql.DB 或 http.Client,就无法 mock——它们是具体类型,且没公开可替换的接口。
正确做法是提取最小接口:
type UserRepository interface {
GetByID(id int) (*User, error)
Create(u *User) error
}
然后让业务逻辑依赖这个接口,而不是具体实现;再写一个 userRepoImpl 结构体去实现它,对接真实数据库。
测试时,你就能写个 mockUserRepo 实现同样接口,按需返回固定数据或错误。
- 别 mock 标准库类型(如
*http.Client),而是封装一层接口 - 接口方法越小越好,避免「大而全」的
Service接口 - mock 对象本身不需要导出,测试文件内定义即可
手动 mock 比 gomock 更快、更轻量
gomock 生成代码多、学习成本高、更新接口后要重新生成,而多数场景下,手写 mock 几行就搞定。
例如模拟上面的 UserRepository:
type mockUserRepo struct {
getFn func(int) (*User, error)
createFn func(*User) error
}
func (m *mockUserRepo) GetByID(id int) (*User, error) {
return m.getFn(id)
}
func (m *mockUserRepo) Create(u *User) error {
return m.createFn(u)
}
使用时直接传入闭包控制行为:
repo := &mockUserRepo{
getFn: func(id int) (*User, error) {
if id == 1 {
return &User{Name: "Alice"}, nil
}
return nil, errors.New("not found")
},
}
这种写法清晰、无额外工具链、IDE 跳转友好,适合 90% 的单元测试场景。
- gomock 更适合大型项目中接口频繁变更、需要强类型校验的场合
- 手写 mock 时,字段命名尽量和原接口方法对应(如
getFn对应GetByID) - 不要在 mock 里加日志或 sleep,那会污染测试行为
测试中注入 mock 的常见方式
Go 不支持构造函数重载或自动 DI,所以注入靠显式参数或字段赋值。两种主流方式:
方式一:函数参数传入依赖(推荐)
func CreateUser(repo UserRepository, name string) error {
u := &User{Name: name}
return repo.Create(u)
}
// 测试
func TestCreateUser(t *testing.T) {
repo := &mockUserRepo{createFn: func(*User) error { return nil }}
err := CreateUser(repo, "Bob")
if err != nil {
t.Fatal(err)
}
}
方式二:结构体字段可设置(适合带状态的服务)
type UserService struct {
repo UserRepository
}
func (s *UserService) FindUser(id int) (*User, error) {
return s.repo.GetByID(id)
}
// 测试前赋值
svc := &UserService{repo: mockRepo}
- 优先选方式一:函数参数最易测试、无副作用、不依赖初始化顺序
- 方式二要注意并发安全:多个测试共用同一实例时,mock 状态可能互相干扰
- 避免在
init()或包级变量中硬编码依赖,那会让测试无法替换
GetByID 返回正常值,却漏了返回 nil, err 的分支。每个 mock 方法调用,都该有至少一个测试覆盖其错误路径。










