單元測試
- 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)改造:
- 聲明一個FileClient的interface,里面包含GetRemoteFile方法。
- 通過依賴注入的方式,將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代碼。簡單使用方法如下:
- go get -u github.com/golang/mock/gomock安裝gomock
- go get -u github.com/golang/mock/mockgen安裝mockgen
- 使用mockgen對需要mock的interface生成mock文件:mockgen -source=xx.go -destination=xx_mock.go -package=xxx
- 在單元測試方法中使用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)路由注冊,真的很方便。推薦大家使用。