Golang單元測試實戰(zhàn)二:依賴注入與面向接口改造

單元測試

  • Unit tests should be pretty lightweight and run fast since they don’t depend on anything external to the unit of code being tested
  • The only way they can fail is by changing the unit of code they are testing

過程

待單元測試代碼:

func (f FileSDK) NewFile() string {
    myClient := ThridPClient{}
    _, err := myClient.GetRemoteFile()
    // do something
    if err.Error() == "error1" {
        return "default1.txt"
    }
    if err.Error() == "error2" {
        return "default2.txt"
    }
    return "default3.txt"
}

其中ThridPClient是一個第三方客戶端SDK。很簡單的,我們會想到構(gòu)造不同client的場景得到不同err,然后覆蓋這個方法的不同場景路徑。
但是存在以下問題:

  • 假如第三方SDK對我們不可見,我們無法知道什么場景會返回什么類型的err
  • 如果第三方SDK的GetRemoteFile方法內(nèi)容變了,那我們這個單元測試很可能會失敗。

為了達(dá)到文章開頭說的單元測試的2個原則,我們需要對以上的業(yè)務(wù)代碼進(jìn)行改造,提高他的可測性。

代碼改造

工作過程中,我們經(jīng)常會說這個代碼沒有可測性。很明顯上述的代碼是沒有可測性的,原因在于2點(diǎn)

  • 第三方client的依賴不是注入的,而是在function內(nèi)部直接實例化的
  • 由于Golang的結(jié)構(gòu)體沒有子類、父類多態(tài)的概念,所以沒有辦法通過一個mock子類繼承client然后重寫GetRemoteFile方法的方式實現(xiàn)mock。需要對代碼進(jìn)行面向接口的改造。

基于以上2點(diǎn),我們對代碼進(jìn)行以下改造:

type FileSDK struct {
    fileClient FileClient
}

func NewFileSDK(fileClient FileClient) *FileSDK {
    return &FileSDK{
        fileClient: fileClient,
    }
}

type FileClient interface {
    GetRemoteFile() (string, error)
}

func (f FileSDK) NewFile() string {
    _, err := f.fileClient.GetRemoteFile()
    // do something
    if err.Error() == "error1" {
        return "default1.txt"
    }
    if err.Error() == "error2" {
        return "default2.txt"
    }
    return "default3.txt"
}

可以看到我們做了以下2點(diǎn)改造:

  1. 聲明一個FileClient的interface,里面包含GetRemoteFile方法。
  2. 通過依賴注入的方式,將interface注入到方法中。

通過以上改造之后,再進(jìn)行單元測試就簡單多了。我們看下單元測試的代碼。

type ThridPClientMock struct {
    fileType int
}

func (c ThridPClientMock) GetRemoteFile() (string, error) {
    if c.fileType == 1 {
        return "", errors.New("error1")
    }
    if c.fileType == 2 {
        return "", errors.New("error2")
    }
    return "xxx.txt", errors.New("")
}

func TestNewFile(t *testing.T) {
    var fileClient ThridPClientMock
    var fileSDK FileSDK
    var result string

    fileClient = ThridPClientMock{1}
    fileSDK = *NewFileSDK(fileClient)
    result = fileSDK.NewFile()
    if result != "default1.txt" {
        t.Errorf("TestNewFile failed: %v", result)
    }

    fileClient = ThridPClientMock{2}
    fileSDK = *NewFileSDK(fileClient)
    result = fileSDK.NewFile()
    if result != "default2.txt" {
        t.Errorf("TestNewFile failed: %v", result)
    }

    fileClient = ThridPClientMock{3}
    fileSDK = *NewFileSDK(fileClient)
    result = fileSDK.NewFile()
    if result != "default3.txt" {
        t.Errorf("TestNewFile failed: %v", result)
    }

}

可以看到,我們可以通過自己實現(xiàn)一個ThridPClientMock,然后通過NewFileSDK將這個mock類的實例注入到我們fileSDK中。因為mock類是我們自己實現(xiàn)的,可以自由控制GetRemoteFile的路由規(guī)則,從而覆蓋不同err返回值的單元測試場景,并且完全不受第三方SDK的代碼實現(xiàn)影響。也就是實現(xiàn)了文章開頭說的單元測試的2個原則。

Golang Mock

以上代碼改造之后,我們的mock類是自己手寫實現(xiàn)的。大家會發(fā)現(xiàn)還是挺麻煩的,所以Golang很暖心的實現(xiàn)了mockgen和gomock,可以幫我們快速生成interface的mock代碼。簡單使用方法如下:

  1. go get -u github.com/golang/mock/gomock安裝gomock
  2. go get -u github.com/golang/mock/mockgen安裝mockgen
  3. 使用mockgen對需要mock的interface生成mock文件:mockgen -source=xx.go -destination=xx_mock.go -package=xxx
  4. 在單元測試方法中使用gomock引用剛剛生成的mock文件里面的方法和結(jié)構(gòu)體,從而達(dá)到mock的目的。

我們來看一下生成的mock代碼:

import (
    reflect "reflect"

    gomock "github.com/golang/mock/gomock"
)

// MockFileClient is a mock of FileClient interface.
type MockFileClient struct {
    ctrl     *gomock.Controller
    recorder *MockFileClientMockRecorder
}

// MockFileClientMockRecorder is the mock recorder for MockFileClient.
type MockFileClientMockRecorder struct {
    mock *MockFileClient
}

// NewMockFileClient creates a new mock instance.
func NewMockFileClient(ctrl *gomock.Controller) *MockFileClient {
    mock := &MockFileClient{ctrl: ctrl}
    mock.recorder = &MockFileClientMockRecorder{mock}
    return mock
}

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

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

// GetRemoteFile indicates an expected call of GetRemoteFile.
func (mr *MockFileClientMockRecorder) GetRemoteFile() *gomock.Call {
    mr.mock.ctrl.T.Helper()
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRemoteFile", reflect.TypeOf((*MockFileClient)(nil).GetRemoteFile))
}

使用方式如下:

func TestNewFile(t *testing.T) {
    var fileSDK FileSDK
    var result string
    var fileClient *MockFileClient

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

    fileClient = NewMockFileClient(ctrl)
    fileClient.EXPECT().GetRemoteFile().Return("", errors.New("error1")) // 進(jìn)行mock打樁
    fileSDK = *NewFileSDK(fileClient)
    result = fileSDK.NewFile()
    if result != "default1.txt" {
        t.Errorf("TestNewFile failed: %v", result)
    }

    fileClient = NewMockFileClient(ctrl)
    fileClient.EXPECT().GetRemoteFile().Return("", errors.New("error2")) // 進(jìn)行mock打樁
    fileSDK = *NewFileSDK(fileClient)
    result = fileSDK.NewFile()
    if result != "default2.txt" {
        t.Errorf("TestNewFile failed: %v", result)
    }

    fileClient = NewMockFileClient(ctrl)
    fileClient.EXPECT().GetRemoteFile().Return("", errors.New("error3")) // 進(jìn)行mock打樁
    fileSDK = *NewFileSDK(fileClient)
    result = fileSDK.NewFile()
    if result != "default3.txt" {
        t.Errorf("TestNewFile failed: %v", result)
    }
}

可以看到gomock通過Controller實現(xiàn)了路由控制,而我們只需要使用EXPECT和Return方法就可以實現(xiàn)路由注冊,真的很方便。推薦大家使用。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容