4. gomock 入门:安装、生成与基础用法

gomock 是 Go 官方维护的 mock 框架,配合 mockgen 代码生成工具,能够根据接口定义自动生成 mock 实现。本章从零开始搭建 gomock 环境,生成第一个 mock 代码,并掌握核心调用模式。

4.1 安装 gomock 与 mockgen

gomock 由两个组件构成:运行时库 github.com/golang/mock/gomock 和代码生成工具 github.com/golang/mock/mockgen。推荐使用 Go 1.16+ 的 go install 方式安装。

组件 安装命令 作用
gomock 库 go get github.com/golang/mock/gomock 提供 Controller、Expect() 等运行时支持
mockgen 工具 go install github.com/golang/mock/mockgen@latest 根据接口生成 mock 结构体代码
版本提示: 如果使用 Go 1.18+ 泛型项目,建议安装 go.uber.org/mock/mockgen(uber 维护的 fork),对泛型接口支持更好。安装方式相同:go install go.uber.org/mock/mockgen@latest。

安装完成后,验证 mockgen 是否在 $GOPATH/bin 或 $GOBIN 中:

mockgen --version
# 输出示例:v1.6.0

4.2 生成第一个 Mock 代码

假设我们有一个用户仓储接口 UserRepository,定义在 internal/repository/user.go 中:

// internal/repository/user.go
package repository

type User struct {
    ID   int64
    Name string
}

type UserRepository interface {
    GetByID(id int64) (*User, error)
    Save(user *User) error
    Delete(id int64) error
}

使用 mockgen 生成 mock 代码,有两种常用方式:

方式一:source 模式(指定源文件)

mockgen -source=internal/repository/user.go \
        -destination=mocks/mock_user.go \
        -package=mocks

方式二:reflect 模式(指定接口全路径)

mockgen -package=mocks \
        -destination=mocks/mock_user.go \
        yourmodule/internal/repository UserRepository

生成后的 mocks/mock_user.go 会包含 MockUserRepository 结构体,以及每个接口方法的 EXPECT() 方法。核心结构如下:

// mocks/mock_user.go (自动生成)
package mocks

import (
    "reflect"
    "yourmodule/internal/repository"
    "github.com/golang/mock/gomock"
)

type MockUserRepository struct {
    ctrl     *gomock.Controller
    recorder *MockUserRepositoryMockRecorder
}

type MockUserRepositoryMockRecorder struct {
    mock *MockUserRepository
}

func NewMockUserRepository(ctrl *gomock.Controller) *MockUserRepository {
    mock := &MockUserRepository{ctrl: ctrl}
    mock.recorder = &MockUserRepositoryMockRecorder{mock}
    return mock
}

func (m *MockUserRepository) EXPECT() *MockUserRepositoryMockRecorder {
    return m.recorder
}

// GetByID 的 mock 方法
func (m *MockUserRepository) GetByID(id int64) (*repository.User, error) {
    ret := m.ctrl.Call(m, "GetByID", id)
    ret0, _ := ret[0].(*repository.User)
    ret1, _ := ret[1].(error)
    return ret0, ret1
}

func (mr *MockUserRepositoryMockRecorder) GetByID(id interface{}) *gomock.Call {
    return mr.mock.ctrl.RecordCall(mr.mock, "GetByID", id)
}
// ... Save、Delete 类似
生成建议: 将 mockgen 命令写入 go generate 注释,放在接口文件顶部://go:generate mockgen -source=user.go -destination=../../mocks/mock_user.go -package=mocks。之后只需执行 go generate ./... 即可批量生成所有 mock。

4.3 基础用法:三段式测试模式

gomock 的测试代码遵循固定的三段式结构:创建 Controller → 设置期望 → 调用并验证。下面是一个完整的测试示例:

// internal/repository/user_test.go
package repository_test

import (
    "testing"
    "github.com/golang/mock/gomock"
    "yourmodule/internal/repository"
    "yourmodule/mocks"
)

func TestGetUser_Success(t *testing.T) {
    // 1. 创建 Controller(负责生命周期和验证)
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()  // 在测试结束时验证所有期望是否满足

    // 2. 创建 mock 实例
    mockRepo := mocks.NewMockUserRepository(ctrl)

    // 3. 设置期望:当调用 GetByID(1) 时,返回指定用户
    expectedUser := &repository.User{ID: 1, Name: "Alice"}
    mockRepo.EXPECT().
        GetByID(int64(1)).
        Return(expectedUser, nil)

    // 4. 调用被测代码(这里直接调用 mock 演示)
    user, err := mockRepo.GetByID(1)

    // 5. 断言结果
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if user.Name != "Alice" {
        t.Errorf("expected Alice, got %s", user.Name)
    }
}
关键规则: ctrl.Finish() 必须在测试函数结束前调用(通常用 defer)。如果某个设置的期望没有被调用,Finish 会触发 t.Fatal 使测试失败。忘记调用 Finish 会导致期望验证被跳过。

4.4 基础匹配器与调用约束

gomock 提供丰富的匹配器(Matcher)来控制期望的触发条件,以及调用次数约束:

类别 匹配器/约束 说明 示例
参数匹配 gomock.Eq(x) 等于指定值(默认行为) GetByID(gomock.Eq(int64(1)))
gomock.Any() 匹配任意参数 Save(gomock.Any())
gomock.Not(x) 不等于指定值 Delete(gomock.Not(int64(0)))
调用次数 .Times(n) 精确调用 n 次 EXPECT().GetByID(1).Times(2)
.MinTimes(n) 至少调用 n 次 EXPECT().Save(…).MinTimes(1)
.MaxTimes(n) 至多调用 n 次 EXPECT().Delete(…).MaxTimes(3)
返回值 .Return(vals…) 设置方法返回值 Return(user, nil)
副作用 .Do(fn) 调用时执行自定义函数 .Do(func(id int64) { log.Println(id) })

4.5 完整流程示意

下图展示了 gomock 在单元测试中的完整工作流程:

gomock 单元测试流程 gomock 单元测试流程 测试函数 gomock.NewController(t) NewMockUserRepository(ctrl) EXPECT().Return() 调用 mock 方法 ctrl.Finish() 验证 关键说明 • defer ctrl.Finish() 确保验证 • 期望未满足 → 测试失败 • 调用顺序默认不敏感

4.6 常见陷阱与最佳实践

  • 陷阱1:忘记调用 Finish — 期望验证不会执行,即使 mock 方法未被调用测试也会通过。始终使用 defer ctrl.Finish()。
  • 陷阱2:参数类型不匹配 — EXPECT().GetByID(1) 默认使用 Eq(1),如果传入 int64(1) 而期望写的是 int(1),会导致匹配失败。建议显式使用 gomock.Eq(int64(1))。
  • 陷阱3:过度指定调用次数 — 对于只关心“至少调用一次”的场景,使用 .MinTimes(1) 而非 .Times(1),避免重构时因调用次数变化而误报。
  • 最佳实践: 将 mock 生成纳入 go generate,每次修改接口后执行 go generate ./... 保持 mock 代码同步。
  • 最佳实践: 在 EXPECT() 链中优先使用 gomock.Any() 匹配不关心的参数,减少测试与实现的耦合。
入门总结: 安装 gomock + mockgen → 在接口文件上执行 mockgen → 在测试中创建 Controller → 设置期望 → 调用并验证。掌握这三步,就掌握了 gomock 的核心工作流。下一节将深入 gomock.Controller 的生命周期管理与高级匹配器。