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 在单元测试中的完整工作流程:
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 的生命周期管理与高级匹配器。