44.1 fake:真实组件或服务的简化实现版替身
fake这个单词的中文含义是“伪造的”“假的”“伪装的”等。在这里,fake测试就是指采用真实组件或服务的简化版实现作为替身,以满足被测代码的外部依赖需求。比如:当被测代码需要连接数据库进行相关操作时,虽然我们在开发测试环境中无法提供一个真实的关系数据库来满足测试需求,但是可以基于哈希表实现一个内存版数据库来满足测试代码要求,我们用这样一个伪数据库作为真实数据库的替身,使得测试顺利进行下去。
Go标准库中的$GOROOT/src/database/sql/fakedb_test.go就是一个sql.Driver接口的简化版实现,它可以用来打开一个基于内存的数据库(sql.fakeDB)的连接并操作该内存数据库:
// $GOROOT/src/database/sql/fakedb_test.go ... type fakeDriver struct { mu sync.Mutex openCount int closeCount int waitCh chan struct{} waitingCh chan struct{} dbs map[string]*fakeDB } ... var fdriver driver.Driver = &fakeDriver{} func init() { Register("test", fdriver) //将自己作为driver进行了注册 } ...
在sql_test.go中,标准库利用上面的fakeDriver进行相关测试:
// $GOROOT/src/database/sql/sql_test.go func TestUnsupportedOptions(t *testing.T) { db := newTestDB(t, "people") defer closeDB(t, db) _, err := db.BeginTx(context.Background(), &TxOptions{ Isolation: LevelSerializable, ReadOnly: true, }) if err == nil { t.Fatal("expected error when using unsupported options, got nil") } } const fakeDBName = "foo" func newTestDB(t testing.TB, name string) *DB { return newTestDBConnector(t, &fakeConnector{name: fakeDBName}, name) } func newTestDBConnector(t testing.TB, fc *fakeConnector, name string) *DB { fc.name = fakeDBName db := OpenDB(fc) if _, err := db.Exec("WIPE"); err != nil { t.Fatalf("exec wipe: %v", err) } if name == "people" { exec(t, db, "CREATE|people|name=string,age=int32,photo=blob,dead=bool, bdate=datetime") exec(t, db, "INSERT|people|name=Alice,age=?,photo=APHOTO", 1) exec(t, db, "INSERT|people|name=Bob,age=?,photo=BPHOTO", 2) exec(t, db, "INSERT|people|name=Chris,age=?,photo=CPHOTO,bdate=?", 3, chrisBirthday) } if name == "magicquery" { exec(t, db, "CREATE|magicquery|op=string,millis=int32") exec(t, db, "INSERT|magicquery|op=sleep,millis=10") } return db }
标准库中fakeDriver的这个简化版实现还是比较复杂,我们再来看一个自定义的简单例子来进一步理解fake的概念及其在Go单元测试中的应用。
在这个例子中,被测代码为包mailclient中结构体类型mailClient的方法:ComposeAndSend:
// chapter8/sources/faketest1/mailclient.go type mailClient struct { mlr mailer.Mailer } func New(mlr mailer.Mailer) *mailClient { return &mailClient{ mlr: mlr, } } // 被测方法 func (c *mailClient) ComposeAndSend(subject string, destinations []string, body string) (string, error) { signTxt := sign.Get() newBody := body + "\n" + signTxt for _, dest := range destinations { err := c.mlr.SendMail(subject, dest, newBody) if err != nil { return "", err } } return newBody, nil }
可以看到在创建mailClient实例的时候,需要传入一个mailer.Mailer接口变量,该接口定义如下:
// chapter8/sources/faketest1/mailer/mailer.go type Mailer interface { SendMail(subject, destination, body string) error }
ComposeAndSend方法将传入的电子邮件内容(body)与签名(signTxt)编排合并后传给Mailer接口实现者的SendMail方法,由其将邮件发送出去。在生产环境中,mailer.Mailer接口的实现者是要与远程邮件服务器建立连接并通过特定的电子邮件协议(如SMTP)将邮件内容发送出去的。但在单元测试中,我们无法满足被测代码的这个要求,于是我们为mailClient实例提供了两个简化版的实现:fakeOkMailer和fakeFailMailer,前者代表发送成功,后者代表发送失败。代码如下:
// chapter8/sources/faketest1/mailclient_test.go type fakeOkMailer struct{} func (m *fakeOkMailer) SendMail(subject string, dest string, body string) error { return nil } type fakeFailMailer struct{} func (m *fakeFailMailer) SendMail(subject string, dest string, body string) error { return fmt.Errorf("can not reach the mail server of dest [%s]", dest) }
下面就是这两个替身在测试中的使用方法:
// chapter8/sources/faketest1/mailclient_test.go func TestComposeAndSendOk(t *testing.T) { m := &fakeOkMailer{} mc := mailclient.New(m) _, err := mc.ComposeAndSend("hello, fake test", []string{"xxx@example.com"}, "the test body") if err != nil { t.Errorf("want nil, got %v", err) } } func TestComposeAndSendFail(t *testing.T) { m := &fakeFailMailer{} mc := mailclient.New(m) _, err := mc.ComposeAndSend("hello, fake test", []string{"xxx@example.com"}, "the test body") if err == nil { t.Errorf("want non-nil, got nil") } }
我们看到这个测试中mailer.Mailer的fake实现的确很简单,简单到只有一个返回语句。但就这样一个极其简化的实现却满足了对ComposeAndSend方法进行测试的所有需求。
使用fake替身进行测试的最常见理由是在测试环境无法构造被测代码所依赖的外部组件或服务,或者这些组件/服务有副作用。fake替身的实现也有两个极端:要么像标准库fakedb_test.go那样实现一个全功能的简化版内存数据库driver,要么像faketest1例子中那样针对被测代码的调用请求仅返回硬编码的成功或失败。这两种极端实现有一个共同点:并不具备在测试前对返回结果进行预设置的能力。这也是上面例子中我们针对成功和失败两个用例分别实现了一个替身的原因。(如果非要说成功和失败也是预设置的,那么fake替身的预设置能力也仅限于设置单一的返回值,即无论调用多少次,传入什么参数,返回值都是一个。)