简介
gomock 的由来和概述
gomock 是 Uber 公司开源的一个 mock 框架,于 2015 年开源。它是基于 Go 语言中的反射机制实现的,可以自动生成接口的 mock 对象。传统的 mock 测试需要手动编写 mock 对象的代码,而 gomock 可以根据接口描述自动生成 mock 代码,大大简化了 mock 对象的创建过程。开发人员只需提供接口描述,gomock 会生成对应的 mock struct,并在测试中直接使用。gomock 的关键优势包括:
- 自动生成 mock 代码:不需要手写 mock 对象代码,提高开发效率
- 类型安全:基于接口自动生成 mock,类型检查友好
- 灵活的匹配:支持自定义 matcher,可以进行灵活的方法参数匹配
- 丰富的 action:支持设置返回值、返回错误等 action
- 便于调试:生成的 mock 代码简洁易读,便于调试
总体来说,gomock 是一个轻量级的 mock 框架,通过自动生成可以减少重复代码的编写,使测试代码更简洁清晰。它的类型安全、灵活的 matcher 和 action 也使 mock 测试更加便捷和可靠。gomock 已经被许多 Go 语言项目采用,是 Go 语言生态里一个流行和实用的 mock 测试框架。
为什么需要使用 gomock
- 效率更高,它可以自动生成 mock 代码,无需开发者手动编写;
- 更安全可靠,基于接口的 mock 对象类型安全,可以充分利用 Go 的接口优势;
- 更灵活可控,可以方便地设置 mock 方法的各种行为,提高测试场景的覆盖率;
总之,gomock 使编写单元测试更高效、更安全和更灵活,是 Go 测试不可或缺的重要框架。它良好的自动化、类型安全和行为可控性,可以大大提升 Go 测试的效率和质量。
gomock 的基本用法
安装
go install go.uber.org/mock/mockgen@latest
定义接口
创建名为 biz.go 的文件,定义下面接口
type INacosRepo interface {
GetConfig(ctx context.Context, dataId string) interface{}
}
type IAdRepo interface {
ESSearchAds(ctx context.Context, grade string, version, endpoint, userStatus int, preview bool, previewUserId int, previewDeviceId string) ([]model.Ad, error)
ESCheckAdPreview(ctx context.Context, previewUserId int, previewDeviceId string) (bool, error)
DBExpireAds(ctx context.Context) error
ESExpireAds(ctx context.Context) error
DBAdPreviewExpire(ctx context.Context) error
ESAdPreviewExpire(ctx context.Context) error
}
生成 mock 对象
mockgen -source=user_pk.go -destination user_pk_mock.go -package biz
通过上面命令生成名为 biz_mock.go 的 mock 文件,代码如下所示
// Code generated by MockGen. DO NOT EDIT.
// Source: biz.go
// Package biz is a generated GoMock package.
package biz
import (
context "context"
model "oralArithmetic/pkg/model"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockINacosRepo is a mock of INacosRepo interface.
type MockINacosRepo struct {
ctrl *gomock.Controller
recorder *MockINacosRepoMockRecorder
}
// MockINacosRepoMockRecorder is the mock recorder for MockINacosRepo.
type MockINacosRepoMockRecorder struct {
mock *MockINacosRepo
}
// NewMockINacosRepo creates a new mock instance.
func NewMockINacosRepo(ctrl *gomock.Controller) *MockINacosRepo {
mock := &MockINacosRepo{ctrl: ctrl}
mock.recorder = &MockINacosRepoMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockINacosRepo) EXPECT() *MockINacosRepoMockRecorder {
return m.recorder
}
// GetConfig mocks base method.
func (m *MockINacosRepo) GetConfig(ctx context.Context, dataId string) interface{} {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetConfig", ctx, dataId)
ret0, _ := ret[0].(interface{})
return ret0
}
// GetConfig indicates an expected call of GetConfig.
func (mr *MockINacosRepoMockRecorder) GetConfig(ctx, dataId interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfig", reflect.TypeOf((*MockINacosRepo)(nil).GetConfig), ctx, dataId)
}
// MockIAdRepo is a mock of IAdRepo interface.
type MockIAdRepo struct {
ctrl *gomock.Controller
recorder *MockIAdRepoMockRecorder
}
// MockIAdRepoMockRecorder is the mock recorder for MockIAdRepo.
type MockIAdRepoMockRecorder struct {
mock *MockIAdRepo
}
// NewMockIAdRepo creates a new mock instance.
func NewMockIAdRepo(ctrl *gomock.Controller) *MockIAdRepo {
mock := &MockIAdRepo{ctrl: ctrl}
mock.recorder = &MockIAdRepoMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockIAdRepo) EXPECT() *MockIAdRepoMockRecorder {
return m.recorder
}
// DBAdPreviewExpire mocks base method.
func (m *MockIAdRepo) DBAdPreviewExpire(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DBAdPreviewExpire", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// DBAdPreviewExpire indicates an expected call of DBAdPreviewExpire.
func (mr *MockIAdRepoMockRecorder) DBAdPreviewExpire(ctx interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DBAdPreviewExpire", reflect.TypeOf((*MockIAdRepo)(nil).DBAdPreviewExpire), ctx)
}
// DBExpireAds mocks base method.
func (m *MockIAdRepo) DBExpireAds(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DBExpireAds", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// DBExpireAds indicates an expected call of DBExpireAds.
func (mr *MockIAdRepoMockRecorder) DBExpireAds(ctx interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DBExpireAds", reflect.TypeOf((*MockIAdRepo)(nil).DBExpireAds), ctx)
}
// ESAdPreviewExpire mocks base method.
func (m *MockIAdRepo) ESAdPreviewExpire(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ESAdPreviewExpire", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// ESAdPreviewExpire indicates an expected call of ESAdPreviewExpire.
func (mr *MockIAdRepoMockRecorder) ESAdPreviewExpire(ctx interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ESAdPreviewExpire", reflect.TypeOf((*MockIAdRepo)(nil).ESAdPreviewExpire), ctx)
}
// ESCheckAdPreview mocks base method.
func (m *MockIAdRepo) ESCheckAdPreview(ctx context.Context, previewUserId int, previewDeviceId string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ESCheckAdPreview", ctx, previewUserId, previewDeviceId)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ESCheckAdPreview indicates an expected call of ESCheckAdPreview.
func (mr *MockIAdRepoMockRecorder) ESCheckAdPreview(ctx, previewUserId, previewDeviceId interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ESCheckAdPreview", reflect.TypeOf((*MockIAdRepo)(nil).ESCheckAdPreview), ctx, previewUserId, previewDeviceId)
}
// ESExpireAds mocks base method.
func (m *MockIAdRepo) ESExpireAds(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ESExpireAds", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// ESExpireAds indicates an expected call of ESExpireAds.
func (mr *MockIAdRepoMockRecorder) ESExpireAds(ctx interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ESExpireAds", reflect.TypeOf((*MockIAdRepo)(nil).ESExpireAds), ctx)
}
// ESSearchAds mocks base method.
func (m *MockIAdRepo) ESSearchAds(ctx context.Context, grade string, version, endpoint, userStatus int, preview bool, previewUserId int, previewDeviceId string) ([]model.Ad, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ESSearchAds", ctx, grade, version, endpoint, userStatus, preview, previewUserId, previewDeviceId)
ret0, _ := ret[0].([]model.Ad)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ESSearchAds indicates an expected call of ESSearchAds.
func (mr *MockIAdRepoMockRecorder) ESSearchAds(ctx, grade, version, endpoint, userStatus, preview, previewUserId, previewDeviceId interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ESSearchAds", reflect.TypeOf((*MockIAdRepo)(nil).ESSearchAds), ctx, grade, version, endpoint, userStatus, preview, previewUserId, previewDeviceId)
}
使用 mock 对象编写测试
func (s *_Suite) TestOralPkBiz_PkRecommend() {
ctx := context.Background()
type args struct {
ctx context.Context
userId int
req request.PkRecommendReq
}
tests := []struct {
name string
args args
wantResp response.PkRecommendResp
wantErr bool
}{
// TODO: Add test cases.
{
name: "test_session_over",
args: args{
ctx: ctx,
userId: 11108,
req: request.PkRecommendReq{
GradeId: "2",
VersionId: "5",
TermId: "1",
},
},
wantResp: response.PkRecommendResp{},
wantErr: false,
},
}
pkInfo := preparePkConfig(ctx)
pkRobotInfo := preparePkRobotConfig(ctx)
s.RepoMock.INacosRepo.EXPECT().GetConfig(ctx, constant.DataIdPkConfig).Return(pkInfo).AnyTimes()
s.RepoMock.INacosRepo.EXPECT().GetConfig(ctx, constant.DataIdPkRobotCfgConfig).Return(pkRobotInfo).AnyTimes()
s.RepoMock.ICacheRepo.EXPECT().Set(ctx, gomock.Any(), gomock.Any(), gomock.Any()).Return(true).AnyTimes()
s.RepoMock.IKnowledgeSearchRepo.EXPECT().SearchPkKnowledge(ctx, gomock.Any()).Return(&elastic.SearchResult{
Hits: &elastic.SearchHits{
Hits: []*elastic.SearchHit{
{Source: []byte(`{"knowledgeId":"2ebf9c53589d43ce966ac52429f52337","knowledgeIdForMerge":["2ebf9c53589d43ce966ac52429f52337"],"knowledgeName":"","example":""}`)},
},
},
}, nil).AnyTimes()
s.RepoMock.IOralPkRepo.EXPECT().GetUserPkScoreById(ctx, gomock.Any()).Return(&model.UserPk{
PkSeasonId: "",
UserId: 0,
PkGrade: 2,
PkSeasonPoints: 100,
}, nil).AnyTimes()
s.RepoMock.IOralPkRepo.EXPECT().GetPkQuestionByKnowledgeIDs(ctx, gomock.Any()).Return([]*model.OralArithmeticQuestion{
{
QuestionId: "2a44e4fb7aee4dbcbfd1f7a6f20a65d2",
},
}, nil).AnyTimes()
s.RepoMock.IOralPkRepo.EXPECT().GetQuestionListByIDs(ctx, gomock.Any()).Return([]*model.OralArithmeticQuestion{
{
Id: 608177,
KnowledgeId: "2ebf9c53589d43ce966ac52429f52337",
QuestionId: "2a44e4fb7aee4dbcbfd1f7a6f20a65d2",
ContentOperated: "5-3=\square",
AnswerOperated: "2",
QuestionType: 1,
QuestionStatus: 2,
QuestionStatusName: "",
Difficulty: 1,
ParseType: 0,
},
}, nil).AnyTimes()
for _, tt := range tests {
s.T().Run(tt.name, func(t *testing.T) {
gotResp, err := s.OralPkBiz.PkRecommend(tt.args.ctx, tt.args.userId, tt.args.req)
if (err != nil) != tt.wantErr {
s.T().Errorf("PkRecommend() error = %v, wantErr %v", err, tt.wantErr)
return
}
s.T().Logf("PkRecommend() gotResp = %v", gotResp)
})
}
}
gomock 的高级用法
提升测试覆盖率
- 增加参数化测试可以通过设置不同的参数来增加测试场景:
func TestAdd(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
adder := NewMockAdder(mockCtrl)
adder.EXPECT().Add(1, 2).Return(3)
adder.EXPECT().Add(3, 4).Return(7)
// ...
}
- 测试错误场景返回错误来测试错误处理:
adder.EXPECT().Add(4, 5).Return(0, errors.New("Error"))
- 模拟异常使用 gomock.Panic() 来模拟 panic:
adder.EXPECT().Add(4, 5).Do(gomock.Panic())
- 设置方法副作用可以通过 Do() 来产生副作用:
var sharedValue int
adder.EXPECT().Add(1, 2).Do(func() {
sharedValue = 10
})
- 并发测试使用 Go 并发功能并发调用:
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
adder.Add(1, 2)
wg.Done()
}()
}
wg.Wait()
模拟不同场景
- 模拟不同的返回值可以通过 Return 方法来设置 mock 方法的不同返回值,模拟不同的返回场景:
mockObj.EXPECT().Get(gomock.Eq(1)).Return(10)
mockObj.EXPECT().Get(gomock.Eq(2)).Return(20)
- 模拟返回错误使用 Return 方法返回 error 对象可以模拟返回错误的场景:
mockObj.EXPECT().Get(gomock.Eq(3)).Return(0, errors.New("not found"))
- 模拟网络延迟使用 Do 方法来添加自定义行为,可以添加 sleep 来模拟网络延迟:
mockObj.EXPECT().Get(gomock.Eq(4)).Do(func() {
time.Sleep(1 * time.Second)
return 30
})
- 模拟 RPC 错误可以返回自定义的 RPC 错误:
mockObj.EXPECT().Get(gomock.Eq(5)).Return(nil, rpctypes.Errorf(codes.NotFound, "not found"))
- 模拟并发问题在 Do 方法中使用共享的状态变量和锁来模拟并发场景:
var mutex sync.Mutex
var count int
mockObj.EXPECT().Get(gomock.Eq(6)).Do(func() {
mutex.Lock()
count++
mutex.Unlock()
return count
})
自定义匹配器
- 实现 Matcher 接口
// 实现 Matcher 接口
type myMatcher struct {}
func (m *myMatcher) Matches(x interface{}) bool {
// 匹配逻辑
}
func (m *myMatcher) String() string {
return "myMatcher"
}
- 定义匹配器函数
func MyMatch(param int) gomock.Matcher {
return &myMatcher{wanted: param}
}
- 在 EXPECT 中使用自定义匹配器
mockObj.EXPECT().Foo(MyMatch(5))
此时 Foo 方法调用时会使用 MyMatch 匹配器进行参数匹配。自定义匹配器的常见场景:
- 正则表达式匹配;
- 对象字段匹配;
- 复杂逻辑匹配自定义匹配器可以让我们摆脱仅通过 Equals 等有限的匹配方式,实现更加灵活和宽松的匹配逻辑。
编写易用的 wrapper
- 定义 interface 编写测试要用的 interface,包含待 mock 的所有方法。
- 实现 wrapper 实现一个结构体,内部含有 gomock 生成的 mock 对象。
- 封装方法在 wrapper 的方法中直接调用内部 mock 对象的方法。
- 提供便捷方法根据需求在 wrapper 中提供一些便捷方法,如默认返回值、可选参数等。示例:
// 1. 接口定义
type Adder interface {
Add(int, int) int
}
// 2. 实现 wrapper
type AdderMock struct {
MockAdder *MockAdder
}
// 3. 封装方法
func (a *AdderMock) Add(x, y int) int {
return a.MockAdder.Add(x, y)
}
// 4. 提供便捷方法
func (a *AdderMock) AddWithDefault() int {
return a.Add(1, 2)
}
// 测试中使用 wrapper
mock := NewAdderMock(ctrl)
mock.AddWithDefault()
这样可以简化 gomock 的使用代码,提高测试用例的复用性和可读性。
gomock 的实现原理
生成 mock 对象的过程
- 解析接口 gomock 会使用 reflect 包解析需要 mock 的接口,获取接口中的方法名、参数和返回值等信息。
- 生成代码根据接口信息,gomock 使用 text/template 自动生成接口方法的 mock 实现代码,包括函数体、匹配器、调用记录等。
- 构建对象使用 go generate 工具自动编译生成的 mock 代码,得到 mock 对象的实现。
- 调节行为测试时通过调用 mock 对象的 EXPECT() 等方法设置返回值、行为等,调节 mock 行为。
- 记录调用 mock 对象记录每次方法调用,assert 是否与预设的行为一致。
gomock 通过自动化代码生成和运行时行为控制,可以快速灵活地构造 mock 对象,减轻编写 mock 对象的工作量。其核心是通过解析接口定义自动生成针对接口的 mock 实现代码。这样可以确保 mock 对象与目标接口一致, typing 安全。
matchers 的工作原理
- 实现 Matcher 接口每个 matcher 都实现了 Matcher 接口,该接口定义了 Matches 和 String 两个方法。
- Matches 方法判断匹配当调用 Expect() 配置 mock 行为时,会用 matcher 的 Matches 方法来判断参数是否匹配。
- String 方法生成代码 matcher 的 String 方法返回其类型名称,gomock 使用该名称生成对应 mock 方法的代码。
- 生成匹配表达式针对每个 Expect() 调用和 matcher,gomock 会生成对应的匹配表达式代码。
- 运行时评估表达式当 mock 方法被调用时,会执行生成的匹配表达式,以判断参数是否满足预期。这样通过匹配器接口和生成的匹配代码,可以实现灵活 parameter matching,如 Eq()、Any()等。
常用的匹配器有:
- Eq() 等于
- Any() 任意参数
- Nil() nil 参数
- Not() 排除指定参数
我们也可以定制 matcher 来实现自定义匹配逻辑。
执行 mock 方法的流程
-
测试代码中调用 mock 对象的方法。
-
根据方法参数,在预设的期望(EXPECT call)中查找匹配的调用记录。
-
如果找到匹配的调用记录,则执行该调用记录配置的行为。
-
如果是返回值,直接返回预设的返回值。
-
如果是函数,则执行函数实现的自定义逻辑。
-
如果没有匹配的调用记录,则测试失败。
-
调用结束后,在当前的 Controller 中记录这次调用。
-
用户可以通过 Controller 查看方法调用次数、参数等信息来进行断言。
-
如果配置了调用后重置期望(After call),则重置对该方法的期望配置。
所以执行流程主要分为:
- 查找匹配的预设期望
- 执行已配置的行为
- 记录调用
- 重置期望这使得我们可以方便地设置 mock 对象的 RETURN、DO 等行为,增强测试的灵活性。
gomock 的优缺点
- 优点:自动生成 mock,提高测试效率等
- 缺点:依赖反射, debug 困难等
总结
gomock 的用途
- 单元测试 - 生成 mock 对象来隔离被测系统,进行更聚焦的测试。
- 集成测试 - 通过 mock 依赖来编写关键路径的集成测试。
- 模拟依赖 - 在本地开发时 mock 真实后端服务,使开发环境独立。
- 验证交互 - 使用 gomock.Controller 来记录并验证 mock 对象的交互行为。
- 测试不确定性 - 配置 mock 对象来模拟错误、延迟等情况。
- 提高覆盖率 - mock 对象可以灵活设置返回值,提高代码覆盖率。
- 模拟容错 - 配置各种错误情况来测试容错能力。
- 增强测试可控性 - 使用 gomock 可以显著提高测试用例的可控性和稳定性。
总之,gomock 适用于各类测试场景,是 Go 语言单元测试不可或缺的重要组件,可以帮助编写更优秀和可靠的测试。