AdSense

網頁

2022/5/18

Golang mock struct methods

Go撰寫測試時對依賴的struct及其方法做mock。


事前要求

參考「Golang 資料庫查詢package目錄分類 」程式。

main.go新增一函式GetEmployeeNumber()依賴EmployeeRepository.GetAllEmployees()查詢全部員工後再取得長度。關鍵在設計時依賴要由外部注入(Dependency Injection)才方便後續的mock,這邊以參數傳入依賴EmployeeRepository

main.go

package main
...
type EmployeeRepository interface {
    GetAllEmployees() ([]model.Employee, error)
    GetEmployeeByID(id int64) (*model.Employee, error)
}

func main() { ... }

func GetEmployeeNumber(er EmployeeRepository) int {
    emps, err := er.GetAllEmployees()
    if err != nil {
        panic("error")
    }
    return len(emps)
}


Mocking

main.go同目錄新增測試檔main_test.go並撰寫用GetEmployeeNumber()的測試程式,利用mock來仿造EmployeeRepository.GetAllEmployees()的結果。

最原本的mock對象為repo/employee.goEmployeeRepositoryImpl.GetAllEmployees(),而mock的第一步是定義介面並讓mock的對象實作該介面。範例已定義介面EmployeeRepositoryEmployeeRepositoryImpl實作。

第二步是建立取代測試對象原依賴的mock。讓mock實作與被mock對象的介面來達到mock。例如下面EmployeeRepositoryMock實作了EmployeeRepository

第三步是在mock中新增與被mock方法相同簽章的函式欄位,並讓mock方法調用此欄位,如此便能彈性地依不同測試設定不一樣的mock方法邏輯。例如下面EmployeeRepositoryMockgetAllEmployeesFn函式欄位與被mock方法EmployeeRepository.GetAllEmployees()的簽章相同,並被mock方法EmployeeRepositoryMock.GetAllEmployees()調用,所以在撰寫測試程式時透過設定getAllEmployeesFn的值來抽換不同的mock邏輯。

main_test.go

package main

import (
    "errors"
    "testing"

    "abc.com/demo/model"
)

// EmployeeRepositoryMock implements EmployeeRepository interface to mock EmployeeRepositoryImpl
type EmployeeRepositoryMock struct {
    // for setting mock impl in test
    getAllEmployeesFn func() ([]model.Employee, error)
    getEmployeeByIDFn func(id int64) (*model.Employee, error)
}

// implements EmployeeRepository.GetAllEmployees
func (erMock *EmployeeRepositoryMock) GetAllEmployees() ([]model.Employee, error) {
    return erMock.getAllEmployeesFn()
}

// implements EmployeeRepository.GetEmployeeByID
func (erMock *EmployeeRepositoryMock) GetEmployeeByID(id int64) (*model.Employee, error) {
    return erMock.getEmployeeByIDFn(id)
}

func TestGetEmployeeNumber(t *testing.T) {
    testCase := struct{ mock, expected int }{10, 10}

    erMock := &EmployeeRepositoryMock{}
    // set mock impl for EmployeeRepositoryMock.GetAllEmployees
    erMock.getAllEmployeesFn = func() ([]model.Employee, error) {
        return make([]model.Employee, testCase.mock), nil
    }
    result := GetEmployeeNumber(erMock)
    if result != testCase.expected {
        t.Errorf("expect %v, but %v", testCase.expected, result)
    }
}

func TestGetEmployeeNumber_Panic(t *testing.T) {
    defer func() {
        if r := recover(); r == nil {
            t.Errorf("expect panic but success")
        }
    }()

    erMock := &EmployeeRepositoryMock{}
    // set mock impl for EmployeeRepositoryMock.GetAllEmployees
    erMock.getAllEmployeesFn = func() ([]model.Employee, error) {
        return nil, errors.New("error")
    }
    GetEmployeeNumber(erMock)
}

github

關係圖。


┌─────────────────────┐
│       Client        │
│ (GetEmployeeNumber) ├ ─ ─ ─ ─ ─ ─┐
└─────────────────────┘         depends
                                   │
                                   ▼
                        ┌───────────────────────┐
                   ┌───►│ (I)EmployeeRepository │◄───┐
                   │    └───────────────────────┘    │
                 impl                              impl
                   │                                 │
       ┌───────────┴────────────┐        ┌───────────┴────────────┐
       │ EmployeeRepositoryMock │        │ EmployeeRepositoryImpl │
       └────────────────────────┘        └────────────────────────┘

專案目錄結構如下。

go-demo/
├── db/
│   └── db.go
├── model/
│   └── employee.go
├── repo/
│   └── employee.go
├── go.mod
├── go.sum
├── main_test.go
└── main.go


測試

專案根目錄輸入go test -v結果如下。

~/../go-demo$ go test -v
=== RUN   TestGetEmployeeNumber
--- PASS: TestGetEmployeeNumber (0.00s)
=== RUN   TestGetEmployeeNumber_Panic
--- PASS: TestGetEmployeeNumber_Panic (0.00s)
PASS
ok      abc.com/demo    0.113s


沒有留言:

AdSense