GO单元测试之一 (GoMock)

2,302 阅读4分钟

为什么需要Mock数据

在协同开发过程中,遇到一些依赖的方法,这时候就需要和其他同事约定好方法的参数及返回值,并且使用mock编写单元测试用例。使用mock有几个好处:

  1. 使用TDD模式,协同模块开发未完成照样可以创建测试环境,进行单元测试。
  2. 与同事之间的协作可以同时进行,不会因为依赖阻塞自测,提前发现问题。
  3. 提高测试覆盖率。
  4. 开发过程中使用mock进行单元测试可以隔离系统,避免数据库中存在脏数据。
  5. 与领域驱动设计(DDD)十分契合。

使用GoMock

安装

GO111MODULE=on go get github.com/golang/mock/mockgen@v1.4.3

文档

安装成功后,可以通过go doc指令查看文档

go doc github.com/golang/mock/gomock

package gomock // import "github.com/golang/mock/gomock"

Package gomock is a mock framework for Go.

Standard usage:

    (1) Define an interface that you wish to mock.
          type MyInterface interface {
            SomeMethod(x int64, y string)
          }
    (2) Use mockgen to generate a mock from the interface.
    (3) Use the mock in a test:
          func TestMyThing(t *testing.T) {
            mockCtrl := gomock.NewController(t)
            defer mockCtrl.Finish()

            mockObj := something.NewMockMyInterface(mockCtrl)
            mockObj.EXPECT().SomeMethod(4, "blah")
            // pass mockObj to a real object and play with it.
          }

By default, expected calls are not enforced to run in any particular order.
Call order dependency can be enforced by use of InOrder and/or Call.After.
Call.After can create more varied call order dependencies, but InOrder is
often more convenient.

The following examples create equivalent call order dependencies.

Example of using Call.After to chain expected call order:

    firstCall := mockObj.EXPECT().SomeMethod(1, "first")
    secondCall := mockObj.EXPECT().SomeMethod(2, "second").After(firstCall)
    mockObj.EXPECT().SomeMethod(3, "third").After(secondCall)

Example of using InOrder to declare expected call order:

    gomock.InOrder(
        mockObj.EXPECT().SomeMethod(1, "first"),
        mockObj.EXPECT().SomeMethod(2, "second"),
        mockObj.EXPECT().SomeMethod(3, "third"),
    )

TODO:

    - Handle different argument/return types (e.g. ..., chan, map, interface).

func InOrder(calls ...*Call)
type Call struct{ ... }
type Controller struct{ ... }
    func NewController(t TestReporter) *Controller
    func WithContext(ctx context.Context, t TestReporter) (*Controller, context.Context)
type GotFormatter interface{ ... }
type GotFormatterFunc func(got interface{}) string
type Matcher interface{ ... }
    func All(ms ...Matcher) Matcher
    func Any() Matcher
    func AssignableToTypeOf(x interface{}) Matcher
    func Eq(x interface{}) Matcher
    func GotFormatterAdapter(s GotFormatter, m Matcher) Matcher
    func Len(i int) Matcher
    func Nil() Matcher
    func Not(x interface{}) Matcher
    func WantFormatter(s fmt.Stringer, m Matcher) Matcher
type StringerFunc func() string
type TestHelper interface{ ... }
type TestReporter interface{ ... }

Example

目录

├── db
│   └── db.go
│   └── db_mock.go
└── repo
    ├── repo.go
    └── repo_test.go

编写

编写依赖的interface方法

打开db/db.go文件,写入以下内容

package db

type User struct {
	ID string `json:"Id"`
	Name string `json:"Name"`
}

type Repository interface {
	Create(key string, value *User) error
	Retrieve(key string) (*User, error)
	Update(key string, value *User) error
	Delete(key string) error
	Count(key string) int32
}

这里利用了多态的概念,用户领域是Repository接口的不同实现方式。因此,在依赖代码未实现的情况下,我们可以通过gomock生成mock文件,进行单元测试。

生成mock文件
mockgen -source=./db/db.go -destination=./db/db_mock.go -package=db

指令含义

  • source:源文件
  • destination:生产mock文件的地址,若未定义,则在控制台中输出
  • package:生成mock文件的包名

同时也支持用反射模式置顶帖接口生成mock文件

mockgen database/sql/driver Conn,Driver
输出的mock文件

/db/db_modk.go

// Code generated by MockGen. DO NOT EDIT.
// Source: ./db/db.go

// Package db is a generated GoMock package.
package db

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

// MockRepository is a mock of Repository interface
type MockRepository struct {
	ctrl     *gomock.Controller
	recorder *MockRepositoryMockRecorder
}

// MockRepositoryMockRecorder is the mock recorder for MockRepository
type MockRepositoryMockRecorder struct {
	mock *MockRepository
}

// NewMockRepository creates a new mock instance
func NewMockRepository(ctrl *gomock.Controller) *MockRepository {
	mock := &MockRepository{ctrl: ctrl}
	mock.recorder = &MockRepositoryMockRecorder{mock}
	return mock
}

// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder {
	return m.recorder
}

// Create mocks base method
func (m *MockRepository) Create(key string, value *User) error {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Create", key, value)
	ret0, _ := ret[0].(error)
	return ret0
}

// Create indicates an expected call of Create
func (mr *MockRepositoryMockRecorder) Create(key, value interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRepository)(nil).Create), key, value)
}

// Retrieve mocks base method
func (m *MockRepository) Retrieve(key string) (*User, error) {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Retrieve", key)
	ret0, _ := ret[0].(*User)
	ret1, _ := ret[1].(error)
	return ret0, ret1
}

// Retrieve indicates an expected call of Retrieve
func (mr *MockRepositoryMockRecorder) Retrieve(key interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Retrieve", reflect.TypeOf((*MockRepository)(nil).Retrieve), key)
}

// Update mocks base method
func (m *MockRepository) Update(key string, value *User) error {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Update", key, value)
	ret0, _ := ret[0].(error)
	return ret0
}

// Update indicates an expected call of Update
func (mr *MockRepositoryMockRecorder) Update(key, value interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepository)(nil).Update), key, value)
}

// Delete mocks base method
func (m *MockRepository) Delete(key string) error {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Delete", key)
	ret0, _ := ret[0].(error)
	return ret0
}

// Delete indicates an expected call of Delete
func (mr *MockRepositoryMockRecorder) Delete(key interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), key)
}

// Count mocks base method
func (m *MockRepository) Count(key string) int32 {
	m.ctrl.T.Helper()
	ret := m.ctrl.Call(m, "Count", key)
	ret0, _ := ret[0].(int32)
	return ret0
}

// Count indicates an expected call of Count
func (mr *MockRepositoryMockRecorder) Count(key interface{}) *gomock.Call {
	mr.mock.ctrl.T.Helper()
	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Count", reflect.TypeOf((*MockRepository)(nil).Count), key)
}
编写业务逻辑

/repo/repo.go

package repo

import (
	db "unit-test/db"
)

type User struct {
	db db.Repository
}

func (u *User) NewUser(key string, user *db.User) error {
	return u.db.Create(key, user)
}

func (u *User) GetUser(key string) (user *db.User, err error)  {
	return u.db.Retrieve(key)
}
编写测试用例
package repo

import (
	"github.com/golang/mock/gomock"
	"testing"
	"unit-test/db"
)

func TestCreateUser(t *testing.T)  {
	ctl := gomock.NewController(t)
	defer ctl.Finish()

    mockUser := db.NewMockRepository(ctl)

    mockUser.EXPECT().Retrieve(gomock.Eq("1")).Return(&db.User{
    	ID: "1",
    	Name: "xavier",
	}, nil).AnyTimes()

    userRepo := &User{
    	db: mockUser,
	}

    user, err := userRepo.GetUser("1")

    if err != nil {
    	t.Errorf("get user error is %+v\n", err)
	} else {
		t.Logf("user is %+v\n", user)
	}
}
  • ctl := db.NewMockRepository(tbl) 实例化mock对象
  • ctl.Finish() 每个控制器都需要调用这个方法,确保mock的断言被引用
  • mockUser.EXPECT() 确保链式调用
  • Retrieve(gomock.Eq("1")) Mock输入参数,并且值必须为"1"
  • Return() 定义返回值
  • AnyTimes() 允许任意次数的调用
运行测试用例
go test ./repo/
测试覆盖率
go test -cover ./repo/