歡迎光臨
每天分享高質量文章

GoMock 測試框架

一、GoMock簡介

1、GoMock簡介

GoMock是由Golang官方開發維護的測試框架,實現了較為完整的基於interface的Mock功能,能夠與Golang內建的testing包良好整合,也能用於其它的測試環境中。GoMock測試框架包含了GoMock包和mockgen工具兩部分,其中GoMock包完成對樁物件生命週期的管理,mockgen工具用來生成interface對應的Mock類源檔案。

GoMock官網:
https://github.com/golang/mock
GoMock安裝:

go get github.com/golang/mock/gomock

mockgen輔助程式碼生成工具安裝:

go get github.com/golang/mock/mockgen

GoMock檔案:

go doc github.com/golang/mock/gomock

2、mockgen使用

(1)mockgen工具選項


mockgen工具支援的選項如下:

-source: 指定介面的源檔案
-destination: mock類程式碼的輸出檔案。如果沒有設定本選項,程式碼將被輸出到標準輸出。-destination選項輸入太長,因此推薦使用重定向符號>將輸出到標準輸出的內容重定向到某個檔案,並且mock類程式碼的輸出檔案的路徑必須是絕對路徑。


-package: 指定mock類源檔案的包名。如果沒有設定本選項,則包名由mock_和輸入檔案的包名級聯而成。


-aux_files: 附加檔案串列用於解析巢狀定義在不同檔案中的interface。指定元素串列以逗號分隔,元素形式為foo=bar/baz.go,其中bar/baz.go是源檔案,foo是-source選項指定的源檔案用到的包名。


-build_flags: 傳遞給build工具的引數
-imports: 依賴的需要import的包
-mock_names:自定義生成mock檔案的串列,使用逗號分割。如Repository=MockSensorRepository,Endpoint=MockSensorEndpoint。
Repository、Endpoint為介面,MockSensorRepository,MockSensorEndpoint為相應的mock檔案。

(2)mockgen工作樣式


mockgen有兩種操作樣式:源檔案樣式和反射樣式。
源檔案樣式透過一個包含interface定義的源檔案生成mock類檔案,透過-source標識開啟,-imports和-aux_files標識在源檔案樣式下是有用的。mockgen源檔案樣式的命令格式如下:


mockgen -source=xxxx.go [other options]

反射樣式透過構建一個程式用反射理解介面生成一個mock類檔案,透過兩個非標誌引數開啟:匯入路徑和用逗號分隔的符號串列(多個interface)。
mockgen反射樣式的命令格式如下:


mockgen packagepath Interface1,Interface2...

第一個引數是基於GOPATH的相對路徑,第二個引數可以為多個interface,並且interface之間只能用逗號分隔,不能有空格。


(3)mockgen工作樣式適用場景


mockgen工作樣式適用場景如下:


A、對於簡單場景,只需使用-source選項。
B、對於複雜場景,如一個源檔案定義了多個interface而只想對部分interface進行mock,或者interface存在巢狀,則需要使用反射樣式。

二、GoMock常用方法

func InOrder(calls ...*Call)

 

InOrder宣告給定呼叫的呼叫順序

 

type Call struct {
t TestReporter // for triggering test failures on invalid call setup

  receiver   interface{}  // the receiver of the method call
method     string       // the name of the method
methodType reflect.Type // the type of the method
args       []Matcher    // the args
origin     string       // file and line number of call setup

  preReqs []*Call // prerequisite calls

  // Expectations
minCalls, maxCalls int

  numCalls int // actual number made


  // actions are called when this Call is called. Each action gets the args and
// can set the return values by returning a non-nil slice. Actions run in the
// order they are created.
actions []func([]interface{}) []interface{}
}

 

Call表示對mock物件的一個期望呼叫

func (c *Call) After(preReq *Call) *Call

After宣告呼叫在preReq完成後執行

func (c *Call) AnyTimes() *Call

允許呼叫0次或多次

func (c *Call) Do(f interface{}) *Call

宣告在匹配時要執行的操作

func (c *Call) MaxTimes(n int) *Call

設定最大的呼叫次數為n次

func (c *Call) MinTimes(n int) *Call

設定最小的呼叫次數為n次

func (c *Call) Return(rets ...interface{}) *Call

Return宣告模擬函式呼叫傳回的值

func (c *Call) SetArg(n int, value interface{}) *Call

SetArg宣告使用指標設定第n個引數的值

func (c *Call) Times(n int) *Call

設定呼叫的次數為n次

func NewController(t TestReporter) *Controller

獲取控制物件

func WithContext(ctx context.Context, t TestReporter) (*Controller, context.Context)

WithContext傳回一個控制器和背景關係,如果發生任何致命錯誤時會取消。

func (ctrl *Controller) Call(receiver interface{}, method string, args ...interface{}) []interface{}

Mock物件呼叫,不應由使用者程式碼呼叫。

func (ctrl *Controller) Finish()

檢查所有預計呼叫的方法是否被呼叫,每個控制器都應該呼叫。本函式只應該被呼叫一次。

func (ctrl *Controller) RecordCall(receiver interface{}, method string, args ...interface{}) *Call

被mock物件呼叫,不應由使用者程式碼呼叫。

func (ctrl *Controller) RecordCallWithMethodType(receiver interface{}, method string,

methodType reflect.Type, args ...interface{}) *Call

被mock物件呼叫,不應由使用者程式碼呼叫。

func Any() Matcher

匹配任意值

func AssignableToTypeOf(x interface{}) Matcher

AssignableToTypeOf是一個匹配器,用於匹配賦值給模擬呼叫函式的引數和函式的引數型別是否匹配。

func Eq(x interface{}) Matcher

透過反射匹配到指定的型別值,而不需要手動設定

func Nil() Matcher

傳回nil

func Not(x interface{}) Matcher

不遞迴給定子匹配器的結果

三、GoMock應用示例

1、interface編寫

定義一個需要mock的介面Repository,infra/db.go檔案如下:

package db


type Repository interface {
Create(key string, value []byte) error
Retrieve(key string) ([]byte, error)
Update(key string, value []byte) error
Delete(key string) error
}

2、mock檔案生成

mockgen生成mock檔案:

mockgen -source=./infra/db.go -destination=./mock/mock_repository.go -package=mock

 

輸出目錄./mock必須存在,否則mockgen會執行失敗。
如果工程中的第三方庫統一放在vendor目錄下,則需要複製一份gomock程式碼到

 

$GOPATH/src/github.com/golang/mock/gomock

 

mockgen命令執行時會在上述路徑訪問gomock。
mock_repository.go檔案如下:

 

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

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

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 []byte) error {
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 {
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) ([]byte, error) {
ret := m.ctrl.Call(m, “Retrieve”, key)
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}

// Retrieve indicates an expected call of Retrieve
func (mr *MockRepositoryMockRecorder) Retrieve(key interface{}) *gomock.Call {
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 []byte) error {
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 {
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 {
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 {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), key)
}

3、MySQL.go檔案

 

package MySQL

import “GoExample/GoMock/infra”

type MySQL struct {
DB db.Repository
}

func NewMySQL(db db.Repository) *MySQL {
return &MySQL;{DB: db}
}

func (mysql *MySQL) CreateData(key string, value []byte) error {
return mysql.DB.Create(key, value)
}

func (mysql *MySQL) GetData(key string) ([]byte, error) {
return mysql.DB.Retrieve(key)
}

func (mysql *MySQL) DeleteData(key string) error {
return mysql.DB.Delete(key)
}


func (mysql *MySQL) UpdateData(key string, value []byte) error {
return mysql.DB.Update(key, value)
}

4、測試用例編寫

生成mock檔案後就可以使用mock物件進行打樁測試,編寫測試用例。
(1)匯入mock相關包

mock相關包包括testing,gomock和mock,import包路徑:

import (
"testing"
"GoExample/GoMock/mock"
"github.com/golang/mock/gomock"
)

 

(2)mock控制器


mock控制器透過NewController介面生成,是mock生態系統的頂層控制,定義了mock物件的作用域和生命週期,以及mock物件的期望。多個協程同時呼叫控制器的方法是安全的。當用例結束後,控制器會檢查所有剩餘期望的呼叫是否滿足條件。

 

ctrl := NewController(t)
defer ctrl.Finish()

 

mock物件建立時需要註入控制器,mock物件註入控制器的程式碼如下:

 

ctrl := NewController(t)
defer ctrl.Finish()
mockRepo := mock_db.NewMockRepository(ctrl)

 

(3)mock物件的行為註入


對於mock物件的行為註入,控制器透過map來維護,一個方法對應map的一項。因為一個方法在一個用例中可能呼叫多次,所以map的值型別是陣列切片。當mock物件進行行為註入時,控制器會將行為Add。當該方法被呼叫時,控制器會將該行為Remove。


如果先Retrieve領域物件失敗,然後Create領域物件成功,再次Retrieve領域物件就能成功。mock物件的行為註入程式碼如下所示:

 

mockRepo.EXPECT().Retrieve(Any()).Return(nil, ErrAny)
mockRepo.EXPECT().Create(Any(), Any()).Return(nil)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes, nil)

 

當批次Create物件時,可以使用Times關鍵字:

 

mockRepo.EXPECT().Create(Any(), Any()).Return(nil).Times(5)

 

當批次Retrieve物件時,需要註入多次mock行為:

 

mockRepo.EXPECT().Retrieve(Any()).Return(objBytes1, nil)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes2, nil)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes3, nil)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes4, nil)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes5, nil)

 

(4)行為呼叫的保序


預設情況下,行為呼叫順序可以和mock物件行為註入順序不一致,即不保序。如果要保序,有兩種方法:


A、透過After關鍵字來實現保序
B、透過InOrder關鍵字來實現保序
透過After關鍵字實現的保序示例程式碼:

 

retrieveCall := mockRepo.EXPECT().Retrieve(Any()).Return(nil, ErrAny)
createCall := mockRepo.EXPECT().Create(Any(), Any()).Return(nil).After(retrieveCall)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes, nil).After(createCall)

 

透過InOrder關鍵字實現的保序示例程式碼:

 

InOrder(
mockRepo.EXPECT().Retrieve(Any()).Return(nil, ErrAny)
mockRepo.EXPECT().Create(Any(), Any()).Return(nil)
mockRepo.EXPECT().Retrieve(Any()).Return(objBytes, nil)
)

 

透過InOrder關鍵字實現保序更簡單,關鍵字InOrder是After的語法糖。

 

func InOrder(calls ...*Call) {
for i := 1; i < len(calls); i++ {
calls[i].After(calls[i-1])
}
}

 

當mock物件行為的註入保序後,如果行為呼叫的順序和其不一致,就會觸發測試失敗。如果在測試用例執行過程中,Repository方法的呼叫順序如果不是按 Retrieve -> Create -> Retrieve的順序進行,則會導致測試失敗。


(5)mock物件的註入


mock物件的行為都註入到控制器後,要將mock物件註入給interface,使得mock物件在測試中生效。


通常,當測試用例執行完成後,並沒有回滾interface到真實物件,有可能會影響其它測試用例的執行,因此推薦使用GoStub框架完成mock物件的註入。

 

stubs := StubFunc(&mysql;,mockdb)
defer stubs.Reset()

 

(6)測試用例編寫


MySQL_test.go檔案

 

package MySQL

import (
“testing”

  “GoExample/GoMock/mock”

  “fmt”

  “github.com/golang/mock/gomock”
)

func TestMySQL_CreateData(t *testing.T) {
ctr := gomock.NewController(t)
defer ctr.Finish()
var key string = “Hello”
var value []byte = []byte(“Go”)
mockRepository := mock_db.NewMockRepository(ctr)
gomock.InOrder(
mockRepository.EXPECT().Create(key, value).Return(nil),
)
mySQL := NewMySQL(mockRepository)
err := mySQL.CreateData(key, value)
if err != nil {
fmt.Println(err)
}
}

func TestMySQL_GetData(t *testing.T) {
ctr := gomock.NewController(t)
defer ctr.Finish()
var key string = “Hello”
var value []byte = []byte(“Go”)
mockRepository := mock_db.NewMockRepository(ctr)
gomock.InOrder(
mockRepository.EXPECT().Retrieve(key).Return(value, nil),
)
mySQL := NewMySQL(mockRepository)
bytes, err := mySQL.GetData(key)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(string(bytes))
}
}

func TestMySQL_UpdateData(t *testing.T) {
ctr := gomock.NewController(t)
defer ctr.Finish()
var key string = “Hello”
var value []byte = []byte(“Go”)
mockRepository := mock_db.NewMockRepository(ctr)
gomock.InOrder(
mockRepository.EXPECT().Update(key, value).Return(nil),
)
mySQL := NewMySQL(mockRepository)
err := mySQL.UpdateData(key, value)
if err != nil {
fmt.Println(err)
}
}


func TestMySQL_DeleteData(t *testing.T) {
ctr := gomock.NewController(t)
defer ctr.Finish()
var key string = "Hello"
mockRepository := mock_db.NewMockRepository(ctr)
gomock.InOrder(
mockRepository.EXPECT().Delete(key).Return(nil),
)
mySQL := NewMySQL(mockRepository)
err := mySQL.DeleteData(key)
if err != nil {
fmt.Println(err)
}
}

5、測試

進入測試用例目錄:

go test .

6、測試結果檢視

生成測試改寫率的 profile 檔案:

go test -coverprofile=cover.out .

利用 profile 檔案生成視覺化介面

go tool cover -html=cover.out

 

本文轉自51CTO部落格作者天山老妖S,

原文連結:http://blog.51cto.com/9291927/2346777

贊(0)

分享創造快樂