官术网_书友最值得收藏!

44.3 mock:專用于行為觀察和驗證的替身

和fake、stub替身相比,mock替身更為強大:它除了能提供測試前的預設置返回結果能力之外,還可以對mock替身對象在測試過程中的行為進行觀察和驗證。不過相比于前兩種替身形式,mock存在應用局限(尤指在Go中)。

  • 和前兩種替身相比,mock的應用范圍要窄很多,只用于實現某接口的實現類型的替身。
  • 一般需要通過第三方框架實現mock替身。Go官方維護了一個mock框架——gomock(https://github.com/golang/mock),該框架通過代碼生成的方式生成實現某接口的替身類型。

mock這個概念相對難于理解,我們通過例子來直觀感受一下:將上面例子中的fake替身換為mock替身。首先安裝Go官方維護的go mock框架。這個框架分兩部分:一部分是用于生成mock替身的mockgen二進制程序,另一部分則是生成的代碼所要使用的gomock包。先來安裝一下mockgen:

$go get github.com/golang/mock/mockgen

通過上述命令,可將mockgen安裝到$GOPATH/bin目錄下(確保該目錄已配置在PATH環境變量中)。

接下來,改造一下mocktest/mailer/mailer.go源碼。在源碼文件開始處加入go generate命令指示符:

// chapter8/sources/mocktest/mailer/mailer.go
//go:generate mockgen -source=./mailer.go -destination=./mock_mailer.go -package=mailer Mailer

package mailer

type Mailer interface {
    SendMail(subject, sender, destination, body string) error
}

接下來,在mocktest目錄下,執行go generate命令以生成mailer.Mailer接口實現的替身。執行完go generate命令后,我們會在mocktest/mailer目錄下看到一個新文件——mock_mailer.go:

// chapter8/sources/mocktest/mailer/mock_mailer.go

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

// mailer包是一個自動生成的 GoMock包
package mailer

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

// MockMailer是Mailer接口的一個模擬實現
type MockMailer struct {
    ctrl     *gomock.Controller
    recorder *MockMailerMockRecorder
}

// MockMailerMockRecorder 是 MockMailer的模擬recorder
type MockMailerMockRecorder struct {
    mock *MockMailer
}

// NewMockMailer創建一個新的模擬實例
func NewMockMailer(ctrl *gomock.Controller) *MockMailer {
    mock := &MockMailer{ctrl: ctrl}
    mock.recorder = &MockMailerMockRecorder{mock}
    return mock
}

// EXPECT返回一個對象,允許調用者指示預期的使用情況
func (m *MockMailer) EXPECT() *MockMailerMockRecorder {
    return m.recorder
}

// SendMail模擬基本方法
func (m *MockMailer) SendMail(subject, sender, destination, body string) error {
    m.ctrl.T.Helper()
    ret := m.ctrl.Call(m, "SendMail", subject, sender, destination, body)
    ret0, _ := ret[0].(error)
    return ret0
}

// SendMail表示預期的對SendMail的調用
func (mr *MockMailerMockRecorder) SendMail(subject, sender, destination, body interface{}) *gomock.Call {
    mr.mock.ctrl.T.Helper()
    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMail", reflect.TypeOf((*MockMailer)(nil).SendMail), subject, sender, destination, body)
}

有了替身之后,我們就以將其用于對ComposeAndSend方法的測試了。下面是使用了mock替身的mailclient_test.go:

// chapter8/sources/mocktest/mocktest/mailclient_test.go
package mailclient

import (
    "errors"
    "testing"

    "github.com/bigwhite/mailclient/mailer"
    "github.com/golang/mock/gomock"
)

var senderSigns = map[string]string{
    "tonybai@example.com":  "I'm a go programmer",
    "jimxu@example.com":    "I'm a java programmer",
    "stevenli@example.com": "I'm a object-c programmer",
}

func TestComposeAndSendOk(t *testing.T) {
    old := getSign
    sender := "tonybai@example.com"
    timestamp := "Mon, 04 May 2020 11:46:12 CST"

    getSign = func(sender string) string {
        selfSignTxt := senderSigns[sender]
        return selfSignTxt + "\n" + timestamp
    }
    defer func() {
        getSign = old //測試完畢后,恢復原值
    }()

    mockCtrl := gomock.NewController(t)
    defer mockCtrl.Finish() //Go 1.14及之后版本中無須調用該Finish

    mockMailer := mailer.NewMockMailer(mockCtrl)
    mockMailer.EXPECT().SendMail("hello, mock test", sender,
     "dest1@example.com",
     "the test body\n"+senderSigns[sender]+"\n"+timestamp).Return(nil).Times(1)
    mockMailer.EXPECT().SendMail("hello, mock test", sender,
     "dest2@example.com",
     "the test body\n"+senderSigns[sender]+"\n"+timestamp).Return(nil).Times(1)

    mc := New(mockMailer)
    _, err := mc.ComposeAndSend("hello, mock test",
      sender, []string{"dest1@example.com", "dest2@example.com"}, "the test body")
    if err != nil {
        t.Errorf("want nil, got %v", err)
    }
}
...

上面這段代碼的重點在于下面這幾行:

mockMailer.EXPECT().SendMail("hello, mock test", sender,
    "dest1@example.com",
    "the test body\n"+senderSigns[sender]+"\n"+timestamp).Return(nil).Times(1)

這就是前面提到的mock替身具備的能力:在測試前對預期返回結果進行設置(這里設置SendMail返回nil),對替身在測試過程中的行為進行驗證。Times(1)意味著以該參數列表調用的SendMail方法在測試過程中僅被調用一次,多一次調用或沒有調用均會導致測試失敗。這種對替身觀察和驗證的能力是mock區別于stub的重要特征。

gomock是一個通用的mock框架,社區還有一些專用的mock框架可用于快速創建mock替身,比如:go-sqlmock(https://github.com/DATA-DOG/go-sqlmock)專門用于創建sql/driver包中的Driver接口實現的mock替身,可以幫助Gopher簡單、快速地建立起對數據庫操作相關方法的單元測試。

小結

本條介紹了當被測代碼對外部組件或服務有強依賴時可以采用的測試方案,這些方案采用了相同的思路:為這些被依賴的外部組件或服務建立替身。這里介紹了三類替身以及它們的適用場合與注意事項。

本條要點如下。

  • fake、stub、mock等替身概念之間并非涇渭分明的,對這些概念的理解容易混淆。比如標準庫net/http/transfer_test.go文件中的mockTransferWriter類型,雖然其名字中帶有mock,但實質上它更像是一個fake替身。
  • 我們更多在包內測試應用上述替身概念輔助測試,這就意味著此類測試與被測代碼是實現級別耦合的,這樣的測試健壯性較差,一旦被測代碼內部邏輯有變化,測試極容易失敗。
  • 通過fake、stub、mock等概念實現的替身參與的測試畢竟是在一個虛擬的“沙箱”環境中,不能代替與真實依賴連接的測試,因此,在集成測試或系統測試等使用真實外部組件或服務的測試階段,務必包含與真實依賴的聯測用例。
  • fake替身主要用于被測代碼依賴組件或服務的簡化實現。
  • stub替身具有有限范圍的、在測試前預置返回結果的控制能力。
  • mock替身則專用于對替身的行為進行觀察和驗證的測試,一般用作Go接口類型的實現的替身。
主站蜘蛛池模板: 黄浦区| 百色市| 开鲁县| 疏附县| 昔阳县| 依安县| 桦甸市| 白城市| 大丰市| 新巴尔虎左旗| 京山县| 嵊州市| 鲜城| 拉萨市| 潼关县| 分宜县| 磴口县| 化隆| 当雄县| 金寨县| 嘉义市| 宾阳县| 运城市| 龙川县| 肇源县| 电白县| 普兰店市| 呈贡县| 曲阳县| 石城县| 彩票| 哈尔滨市| 确山县| 泾阳县| 农安县| 清流县| 明溪县| 高唐县| 佛山市| 灵川县| 莱州市|