Skip to content

Latest commit

 

History

History
257 lines (184 loc) · 6.7 KB

gomock.md

File metadata and controls

257 lines (184 loc) · 6.7 KB

Introduction

在實際開發專案進行單元測試的時候, 卻往往發現有一大堆的依賴, 這時就是 gomock 大顯身手的時候了

Gomock 為 Go 的一個 mock framework, 由官方提供並維護

Installation

$ go get -u github.com/golang/mock/gomock
$ go install github.com/golang/mock/mockgen

首先需要安裝 gomock 和 mock 程式碼的生成工具 mockgen, 後者可以大幅節省工作量, 只需要瞭解其使用方式即可

Usage

mockgen 指令中支援兩種生成模式:

  • source: 從 source code 生成 mock interface (通過 -source 啟用)

    mockgen -source=foo.go [other options]
  • reflect: 通過使用反射來生成 mock interface, 其通過傳遞兩個非標誌參數啟用: 導入路徑和以逗號區隔的 interface 列表

    mockgen database/sql/driver Conn,Driver

本質上來說上述兩種模式生成的 mock 程式碼並無區別, 選擇合適的使用即可

Test Cases

下面會示範模擬一個簡單的 test case 以熟悉整體的測試流程

測試步驟如下:

  • 構思整體測試邏輯
  • 定義想要模擬的依賴項的 interface
  • 使用 mockgen 指令對所需 mock 的 interface 生成 mock 文件
  • 編寫單元測試邏輯, 在測試中使用 mock
  • 進行單元測試驗證

Define Interface

person/male.go 檔案定義 Male interface:

package person

type Male interface {
    Get(id int64) error
}

Call Method

user/user.go 檔案中調用 MaleGet() 方法:

package user

type User struct {
    Person person.Male
}

func NewUser(p person.Male) *User {
    return &User{Person: p}
}

func (u *User) GetUserInfo(id int64) error {
    return u.Person.Get(id)
}

Generate Mock File

回到根目錄執行以下指令:

$ mockgen -source=./person/male.go -destination=./mock/male_mock.go -package=mock
  • source: 設置需要 mock 的 interface 檔案
  • destination: 設置 mock 檔案輸出的位置, 默認輸出 Stdout
  • package: 設置 mock 文件的 package name, 默認為 mock_filename(mock_person)
  • 完整參數參考官方手冊

執行完畢後發現 mock/ 目錄下多了一個 male_mock.go 的檔案, 即為 mock 檔案

輸出後的 mock file 如下:

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

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

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

// MockMale is a mock of Male interface
type MockMale struct {
    ctrl     *gomock.Controller
    recorder *MockMaleMockRecorder
}

// MockMaleMockRecorder is the mock recorder for MockMale
type MockMaleMockRecorder struct {
    mock *MockMale
}

// NewMockMale creates a new mock instance
func NewMockMale(ctrl *gomock.Controller) *MockMale {
    mock := &MockMale{ctrl: ctrl}
    mock.recorder = &MockMaleMockRecorder{mock}
    return mock
}

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

// Get mocks base method
func (m *MockMale) Get(id int64) error {
    ret := m.ctrl.Call(m, "Get", id)
    ret0, _ := ret[0].(error)
    return ret0
}

// Get indicates an expected call of Get
func (mr *MockMaleMockRecorder) Get(id interface{}) *gomock.Call {
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockMale)(nil).Get), id)
}

Write Test Cases

user/user_test.go 檔案編寫 test cases:

package user

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

    var id int64 = 1
    mockMale := mock.NewMockMale(ctl)
    gomock.InOrder(
        mockMale.EXPECT().Get(id).Return(nil),
    )

    user := NewUser(mockMale)
    err := user.GetUserInfo(id)
    if err != nil {
        t.Errorf("user.GetUserInfo err: %v", err)
    }
}
  • gomock.NewController: 返回 gomock.Controller, 其代表 mock 生態系統中的頂層控件, 定義了 mock 物件的範圍, 生命週期和期望值, 另其為 thread-safe
  • mock.NewMockMale: 創建一個新的 mock instance
  • gomock.InOrder: 聲明給定的調用應按順序進行(為對 gomock.After 的二次封裝)
  • mockMale.EXPECT().Get(id).Return(nil):
    • EXPECT(): 返回一個允許調用者設置期望值返回值的物件
    • Get(id): 設置 input parameter 並調用 mock instance 的方法
    • Return(nil): 設置先前調用方法的 output parameter
  • NewUser(mockMale): 創建 User instance, 這裡注入 mock 物件, 因此實際上在後面的 user.GetUserInfo(id) 調用中調用的是事先模擬好的 mock 方法 (input parameter id 為 1)
  • ctl.Finish(): 進行 mock instance 的期望值斷言, 一般會使用 defer 延遲執行以確保執行

Run Test

回到根目錄並執行以下指令:

$ go test ./user
ok      github.com/regy/mockd/user

即完成單元測試, 可以透過調整 Return() 返回值來得到不一樣的測試結果

Test Coverage

$ go test -cover ./user
ok      github.com/regy/mockd/user    (cached)    coverage: 100.0% of statements

可以通過設置 -cover 參數來開啟 coverage ratio 的統計

Web Dashboard

  • 生成 test coverage 的 profile 檔案:

    $ go test ./... -coverprofile=cover.out
  • 利用 profile 檔案生成可視化介面:

    $ go tool cover -html=cover.out

即可透過可視化介面查看 test coverage 情況

Other

Mock Methods

調用方法:

  • Call.Do(): 聲明匹配時要運行的操作
  • Call.DoAndReturn(): 聲明匹配時要運行的操作, 且模擬返回該函式的返回值
  • Call.MaxTimes(): 設置最大的調用次數為 n 次
  • Call.MinTimes(): 設置最小的調用次數為 n 次
  • Call.AnyTimes(): 允許調用次數為 0 或更多次
  • Call.Times(): 設置調用次數為 n 次

參數匹配:

  • gomock.Any(): 匹配任意值
  • gomock.Eq(): 通過反射匹配到指定型別值, 無需手動設置
  • gomock.Nil(): 返回 nil

💡 https://godoc.org/github.com/golang/mock/gomock#pkg-index

Generate Multiple Mock Files

可以利用 go:generate 來完成批量處理功能:

go generate [-run regexp] [-n] [-v] [-x] [build flags] [file.go... | packages]