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 比 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 的「边界覆盖」——比如只测了 GetByID 返回正常值,却漏了返回 nil, err 的分支。每个 mock 方法调用,都该有至少一个测试覆盖其错误路径。