



和fake、stub替身相比,mock替身更为强大:它除了能提供测试前的预设置返回结果能力之外,还可以对mock替身对象在测试过程中的行为进行观察和验证。不过相比于前两种替身形式,mock存在应用局限(尤指在Go中)。
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简单、快速地建立起对数据库操作相关方法的单元测试。
本条介绍了当被测代码对外部组件或服务有强依赖时可以采用的测试方案,这些方案采用了相同的思路:为这些被依赖的外部组件或服务建立 替身 。这里介绍了三类替身以及它们的适用场合与注意事项。
本条要点如下。