Go测试中如何模拟数据_Go Mock基础思路讲解

10次阅读

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

Go测试中如何模拟数据_Go Mock基础思路讲解

Go 测试中为什么不能直接 new 一个真实依赖?

因为真实依赖(比如数据库http 客户端、文件系统)会破坏测试的隔离性、速度和可重复性。调用 sql.Open 可能连接失败,http.Get 可能超时或返回非预期状态,这些都不是单元测试该关心的。

核心原则:测试只验证被测函数的逻辑,不验证第三方服务是否在线或响应是否正确。

所以必须把依赖「替换」成可控的模拟实现——不是靠全局变量赋值或反射修改,而是通过接口抽象 + 依赖注入来实现。

mock 的前提是定义 Interface,不是 Struct

Go 的 mock 本质是实现同名方法签名的假对象。如果你的生产代码直接依赖 *sql.DBhttp.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() 或包级变量中硬编码依赖,那会让测试无法替换

测试里最容易被忽略的,是 mock 的「边界覆盖」——比如只测了 GetByID 返回正常值,却漏了返回 nil, err 的分支。每个 mock 方法调用,都该有至少一个测试覆盖其错误路径。

text=ZqhQzanResources