AdSense

網頁

2021/7/17

Golang 對package function做mock

當程式中呼叫另外一個package的函式,則測試時對這個package函式mock的方式如下。


範例環境:

  • Go 1.16
  • Testify 1.7

範例專案的module名稱為abc.com/demo


主程式

以下為主程式的函式、型別及檔案。

model/employee.go僅定義一個Employee struct型別,為其他函式中使用的參數。

model/employee.go

package model

type Employee struct {
    Id   int
    Name string
    Age  int
}

service/calculator.go中定義了Plus()函式為測試時要mock的對象。

service/calculator.go

package serivce

func Plus(x, y int) int {
    return x + y
}

demo.go的函式AddAge()為下面demo_test.go測試的對象,裡面呼叫了service.Plus()

demo.go

package demo

import (
    "errors"

    "abc.com/demo/model"
    "abc.com/demo/serivce"
)

func AddAge(x int, emp model.Employee) (int, error) {
    if (emp == model.Employee{}) {
        return -1, errors.New("emp is empty")
    }

    return serivce.Plus(x, emp.Age), nil // call package function
}


測試

若要對AddAge()中的service.Plus()做mock則需要對程式進行調整,有以下幾種方式。


方式一:變數mock

把依賴的函式放到package variable變數,改為呼叫變數的函式。例如下面把service.Plus()放入plus變數中,AddAge()中則是呼叫plus()

demo.go

package demo

import (
    "errors"

    "abc.com/demo/model"
    "abc.com/demo/serivce"
)

var plus = serivce.Plus // assign dependent function to package variable

func AddAge(x int, emp model.Employee) (int, error) {
    if (emp == model.Employee{}) {
        return -1, errors.New("emp is empty")
    }

    return plus(x, emp.Age), nil // call package variable function
}

測試AddAge()時即可利用package variable plusservice.Plus()進行mock。在測試程式中以mock函式替換主程式的plus變數內容,並將原函式暫存起來,測試結束完後再復原。

demo_test.go

package demo

import (
    "testing"

    "abc.com/demo/model"
    "github.com/stretchr/testify/assert"
)

func TestAddAge(t *testing.T) {

    testCase := struct {
        x        int
        emp      model.Employee
        expected int
    }{
        1,
        model.Employee{Id: 1, Name: "John", Age: 33},
        34,
    }

    // create mock function
    plusMock := func(x, y int) int {
        return 34
    }

    originalPlus := plus // store orignal function to another variable
    plus = plusMock      // replace original function by mock function

    // restore package variable after test finished
    defer func() { plus = originalPlus }()

    actual, _ := AddAge(testCase.x, testCase.emp)

    assert.Equal(t, testCase.expected, actual) // PASS
}


方式二:參數mock

調整demo.go程式,定義一個函式型態PlusFunc,且AddAge()函式傳入PlusFunc()參數作為內部呼叫的對象。

demo.go

package demo

import (
    "errors"

    "abc.com/demo/model"
)

type PlusFunc func(x, y int) int

func AddAge(x int, emp model.Employee, plus PlusFunc) (int, error) {
    if (emp == model.Employee{}) {
        return -1, errors.New("emp is empty")
    }
    return plus(x, emp.Age), nil
}

在測試程式即可對PlusMock參數做mock並傳入AddAge()達到mock效果。

demo_test.go

package demo

import (
    "testing"

    "abc.com/demo/model"
    "github.com/stretchr/testify/assert"
)

func TestAddAge(t *testing.T) {

    testCase := struct {
        x        int
        emp      model.Employee
        expected int
    }{
        1,
        model.Employee{Id: 1, Name: "John", Age: 33},
        34,
    }

    plusMock := func(x, y int) int {
        return 34
    }

    actual, _ := AddAge(testCase.x, testCase.emp, plusMock)

    assert.Equal(t, testCase.expected, actual) // PASS
}


方式三:介面mock - 型別屬性

調整calculator.goPlus()函式為Calculator struct型別的方法,實作CalculatorService介面。

service/calculator.go

package serivce

type CalculatorService interface {
    Plus(int, int) int
}

type Calculator struct {
}

func (c Calculator) Plus(x, y int) int {
    return x + y
}

調整demo.goAddAge()函式為Demo struct型別的方法。Demo的屬性serivce.CalculatorService,即calculator.go中定義的介面。

demo.go

package demo

import (
    "errors"

    "abc.com/demo/model"
    "abc.com/demo/serivce"
)

type Demo struct {
    service serivce.CalculatorService
}

func (demo *Demo) AddAge(x int, emp model.Employee) (int, error) {
    if (emp == model.Employee{}) {
        return -1, errors.New("emp is empty")
    }
    return demo.service.Plus(x, emp.Age), nil
}

在測試程式使用Testify的mock.Mock取代被測對象的中的依賴,並利用mock.On()設計呼叫mock方法的預期的輸入參數及回傳值。

demo_test.go

package demo

import (
    "testing"

    "abc.com/demo/model"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
)

// define mock type
type CalculatorMock struct {
    mock.Mock
}

// use mock to implments CalculatorService's method
func (calMock *CalculatorMock) Plus(x, y int) int {
    args := calMock.Called(x, y)
    return args.Int(0)
}
func TestAddAge(t *testing.T) {

    testCase := struct {
        x        int
        emp      model.Employee
        expected int
    }{
        1,
        model.Employee{Id: 1, Name: "John", Age: 33},
        34,
    }

    calMock := new(CalculatorMock)       // create mock instance
    calMock.On("Plus", 1, 33).Return(34) // setup mock method arguments and return value

    demo := Demo{calMock} // create struct inject mock
    actual, _ := demo.AddAge(testCase.x, testCase.emp)

    assert.Equal(t, testCase.expected, actual)
}


方式四:介面mock - 函式參數

類似上面對型別屬性做mock,差別在於這邊是把依賴介面作為參數傳入受測對象。

調整calculator.goPlus()函式為Calculator struct型別的方法,實作CalculatorService介面。

service/calculator.go

package serivce

type CalculatorService interface {
    Plus(int, int) int
}

type Calculator struct {
}

func (c Calculator) Plus(x, y int) int {
    return x + y
}

調整demo.goAddAge()函式傳入參數CalculatorService,即calculator.go中定義的介面。

demo.go

package demo

import (
    "errors"

    "abc.com/demo/model"
    "abc.com/demo/serivce"
)

func AddAge(x int, emp model.Employee, calService serivce.CalculatorService) (int, error) {
    if (emp == model.Employee{}) {
        return -1, errors.New("emp is empty")
    }
    return calService.Plus(x, emp.Age), nil
}

在測試程式使用Testify的mock.Mock取代被測對象的中的依賴,並利用mock.On()設計呼叫mock方法的預期的輸入參數及回傳值。

demo_test.go

package demo

import (
    "testing"

    "abc.com/demo/model"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
)

type CalculatorMock struct {
    mock.Mock
}

func (calMock *CalculatorMock) Plus(x, y int) int {
    args := calMock.Called(x, y)
    return args.Int(0)
}

func TestAddAge(t *testing.T) {

    testCase := struct {
        x        int
        emp      model.Employee
        expected int
    }{
        1,
        model.Employee{Id: 1, Name: "John", Age: 33},
        34,
    }

    calMock := new(CalculatorMock)       // create mock instance
    calMock.On("Plus", 1, 33).Return(34) // setup mock method arguments

    actual, _ := AddAge(testCase.x, testCase.emp, calMock)

    assert.Equal(t, testCase.expected, actual) // PASS
}


沒有留言:

AdSense