Go语⾔Mock使⽤基本指南详解
当前的实践中问题
在项⽬之间依赖的时候我们往往可以通过mock⼀个接⼝的实现,以⼀种⽐较简洁、独⽴的⽅式,来进⾏测试。但是在mock使⽤的过程中,因为⼤家的风格不统⼀,⽽且很多使⽤minimal implement的⽅式来进⾏mock,这就导致了通过mock出的实现各个函数的返回值往往是静态的,就⽆法让caller根据返回值进⾏的⼀些复杂逻辑。
⾸先来举⼀个例⼦
package task
type Task interface {
Do(int) (string, error)
}
通过minimal implement的⽅式来进⾏⼿动的mock
package mock
type MinimalTask struct {
// filed
}
func NewMinimalTask() *MinimalTask {
return &MinimalTask{}
}
func (mt *MinimalTask) Do(idx int) (string, error) {
return "", nil
}
在其他包使⽤Mock出的实现的过程中,就会给测试带来⼀些问题。
举个例⼦,假如我们有如下的接⼝定义与函数定义
package pool
import "/ultramesh/mock-example/task"
张兴才type TaskPool interface {
Run(times int) error
}
type NewTask func() task.Task
我们基于接⼝定义和接⼝构造函数定义,封装了⼀个实现
package pool
import (
"fmt"
"/pkg/errors"
"/ultramesh/mock-example/task"
)
type TaskPoolImpl struct {
pool []task.Task
}
func NewTaskPoolImpl(newTask NewTask, size int) *TaskPoolImpl {
tp := &TaskPoolImpl{
pool: make([]task.Task, size),
}
for i := 0; i < size; i++ {
tp.pool[i] = newTask()
}
return tp
}
func (tp *TaskPoolImpl) Run(times int) error {
poolLen := len(tp.pool)
for i := 0; i < times; i++ {
ret, err := tp.pool[i%poolLen].Do(i)
if err != nil {
// process error
return errors.Wrap(err, fmt.Sprintf("error while run task %d", i%poolLen))
}
switch ret {
ca "":
// process 0
fmt.Println(ret)
ca "a":
// process 1
fmt.Println(ret)
ca "b":
田园日记7
// process 2
fmt.Println(ret)
ca "c":
// process 3
fmt.Println(ret)
}
}
return nil
}
接着我们来写测试的话应该是下⾯
package pool谣
import (
"/golang/mock/gomock"
"/stretchr/testify/asrt"
"/ultramesh/mock-example/mock"
"/ultramesh/mock-example/task"
"testing"
)
我的母亲我的家type TestSuit struct {
name string
newTask NewTask
size int
times int
}
func TestTaskPoolRunImpl(t *testing.T) {
testSuits := []TestSuit{
{
nam
e: "minimal task pool",
newTask: func() task.Task { return mock.NewMinimalTask() },
size: 100,
times: 200,
},
}
for _, suit := range testSuits {
t.Run(suit.name, func(t *testing.T) {
var taskPool TaskPool = wTask, suit.size)
err := taskPool.Run(suit.size)
asrt.NoError(t, err)
})
}
}
这样通过go test⾃带的覆盖率测试我们能看到TaskPoolImpl实际被测试到的路径为
可以看到的⼿动实现MinimalTask的问题在于,由于对于caller来说,callee的返回值是不可控的,我们只能覆盖到由MinimalTask所定死的返回值的路径,此外mock在我们的实践中往往由被依赖的项⽬来操作,他不知道caller怎样根据返回值进⾏处理,没有办法封装出⼀个简单、够⽤的最⼩实现供接⼝测试使⽤,因此我们需要改进我们mock策略,使⽤golang官⽅的mock⼯具——gomock来进⾏更好地接⼝测试。
gomock实践
我们使⽤golang官⽅的mock⼯具的优势在于
我们可以基于⼯具⽣成的mock代码,我们可以⽤⼀种更精简的⽅式,封装出⼀个minimal implement,完成和⼿⼯实现⼀个minimal implement⼀样的效果。
可以允许caller⾃⼰灵活地、有选择地控制⾃⼰需要⽤到的那些接⼝⽅法的⼊参以及出参。
还是上⾯TaskPool的例⼦,我们现在使⽤gomock提供的⼯具来⾃动⽣成⼀个mock Task mockgen -destination mock/ -package mock -source
在mock包中⽣成⼀个来实现接⼝Task
⾸先基于,我们可以实现⼀个MockMinimalTask⽤于最简单的测试package mock
import "/golang/mock/gomock"
func NewMockMinimalTask(ctrl *gomock.Controller) *MockTask {
mock := NewMockTask(ctrl)
mock.EXPECT().Do().Return("", nil).AnyTimes()
return mock
}
于是这样我们就可以实现⼀个MockMinimalTask⽤来做⼀些测试
package pool
import (
"/golang/mock/gomock"
"/stretchr/testify/asrt"
"/ultramesh/mock-example/mock"
"/ultramesh/mock-example/task"
"testing"
)
type TestSuit struct {
name string
newTask NewTask
size int
times int
}
func TestTaskPoolRunImpl(t *testing.T) {
testSuits := []TestSuit{
//{
// name: "minimal task pool",
// newTask: func() task.Task { return mock.NewMinimalTask() },
// size: 100,
// times: 200,
//},
{
name: "mock minimal task pool",
newTask: func() task.Task { return mock.NewMockMinimalTask(ctrl) },
size: 100,
times: 200,
},
}
for _, suit := range testSuits {
t.Run(suit.name, func(t *testing.T) {
var taskPool TaskPool = wTask, suit.size)
err := taskPool.Run(suit.size)
asrt.NoError(t, err)
})
}
}
我们使⽤这个新的测试⽂件进⾏覆盖率测试
可以看到测试结果是⼀样的,那当我们想要达到更⾼的测试覆盖率的时候应该怎么办呢?我们进⼀步修改测试package pool
import (
"errors"
"/golang/mock/gomock"
"/stretchr/testify/asrt"
"/ultramesh/mock-example/mock"
"/ultramesh/mock-example/task"
"testing"
)
type TestSuit struct {
name string
newTask NewTask
size int
times int
isErr bool
}
func TestTaskPoolRunImpl_MinimalTask(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
testSuits := []TestSuit{
//{
// name: "minimal task pool",
/
/ newTask: func() task.Task { return mock.NewMinimalTask() },
// size: 100,
// times: 200,
//},
{
name: "mock minimal task pool",
newTask: func() task.Task { return mock.NewMockMinimalTask(ctrl) },
size: 100,
times: 200,
},
{
name: "return err",
newTask: func() task.Task {
mockTask := mock.NewMockTask(ctrl)
// 加⼊了返回错误的逻辑
mockTask.EXPECT().Do(gomock.Any()).Return("", errors.New("return err")).AnyTimes()
return mockTask
彩虹小镇
},
size: 100,
times: 200,
isErr: true,
},
红杏出墙什么意思}
for _, suit := range testSuits {
t.Run(suit.name, func(t *testing.T) {
var taskPool TaskPool = wTask, suit.size)
err := taskPool.Run(suit.size)
火影忍者香磷if suit.isErr {
asrt.Error(t, err)
女性养生茶
} el {
asrt.NoError(t, err)
}
})
}
}
这样我们就能够覆盖到error的处理逻辑