序言
對(duì)于領(lǐng)域?qū)ο蟮腢T測(cè)試來(lái)說(shuō)唱逢,基礎(chǔ)設(shè)施層(infra)的操作函數(shù)都應(yīng)該被打樁。對(duì)于Golang來(lái)說(shuō),大家通常會(huì)想到GoMock迂求。GoMock是由Golang官方開(kāi)發(fā)維護(hù)的針對(duì)Golang的Mock框架,代碼在github.com上托管狞甚。GoMock目前已經(jīng)實(shí)現(xiàn)了較為完整的基于interface的Mock功能锁摔,能夠與Golang內(nèi)建的testing包良好集成,使用GoMock編寫的測(cè)試文件哼审,能夠直接使用go test命令進(jìn)行測(cè)試谐腰,用戶API簡(jiǎn)單友好。
盡管GoMock非常優(yōu)秀涩盾,但是對(duì)于普通的函數(shù)打樁來(lái)說(shuō)也有一些缺點(diǎn):
- 必須引入額外的抽象(interface)
- 打樁過(guò)程比較重
- 既有代碼必須適配新增的抽象
我們知道十气,Golang支持閉包,這使得函數(shù)可以作為另一個(gè)函數(shù)的參數(shù)或返回值春霍,而且可以賦值給一個(gè)變量砸西。
閉包的特性使得筆者想到了Stub,于是開(kāi)始了本文的體驗(yàn)址儒。
Exec函數(shù)
Exec是infra層的一個(gè)操作函數(shù)芹枷,實(shí)現(xiàn)很簡(jiǎn)單,代碼如下所示:
func Exec(cmd string, args ...string) (string, error) {
cmdpath, err := exec.LookPath(cmd)
if err != nil {
log.Errorf("exec.LookPath err: %v, cmd: %s", err, cmd)
return "", infra.ERR_EXEC_LOOKPATH_FAILED
}
var output []byte
output, err = exec.Command(cmdpath, args...).CombinedOutput()
if err != nil {
log.Errorf("exec.Command.CombinedOutput err: %v, cmd: %s", err, cmd)
return "", infra.ERR_EXEC_COMBINED_OUTPUT_FAILED
}
log.Info("CMD[", cmdpath, "]ARGS[", args, "]OUT[", string(output), "]")
return string(output), nil
}
Stub設(shè)計(jì)
物理設(shè)計(jì)
stub位于test目錄下莲趣,和*test目錄或文件并行為*stub鸳慈,比如
test/infra-test/os-encap-test/exec_test.go ==>
test/infra-stub/os-encap-stub/exec_stub.go
既有函數(shù)重構(gòu)
Exec函數(shù)的重構(gòu)非常簡(jiǎn)單,only and only:
- 在函數(shù)前面新增“var ="
- 將函數(shù)名Exec移動(dòng)到”=“前面
// infra/os-encap/exec.go
var Exec = func(cmd string, args ...string) (string, error) {
cmdpath, err := exec.LookPath(cmd)
if err != nil {
log.Errorf("exec.LookPath err: %v, cmd: %s", err, cmd)
return "", infra.ERR_EXEC_LOOKPATH_FAILED
}
var output []byte
output, err = exec.Command(cmdpath, args...).CombinedOutput()
if err != nil {
log.Errorf("exec.Command.CombinedOutput err: %v, cmd: %s", err, cmd)
return "", infra.ERR_EXEC_COMBINED_OUTPUT_FAILED
}
log.Info("CMD[", cmdpath, "]ARGS[", args, "]OUT[", string(output), "]")
return string(output), nil
}
這樣簡(jiǎn)單的重構(gòu)后喧伞,還有一個(gè)大的優(yōu)勢(shì)是Exec函數(shù)的調(diào)用將保持不變走芋。
Stub函數(shù)
Exec的Stub函數(shù)我們命名為ExecInject绩郎,它的設(shè)計(jì)和實(shí)現(xiàn)非常簡(jiǎn)單,即ExecInject函數(shù)有多個(gè)入?yún)⑽坛眩瑳](méi)有返回值肋杖,而且入?yún)⒘斜砗虴xec函數(shù)的返回值列表一一對(duì)應(yīng),代碼如下所示:
// test/infra-stub/oscap-stub/exec_stub.go
func ExecInject(output string, err error) {
osencap.Exec = func(cmd string, args ...string) (string, error) {
return output, err
}
}
Stub序列函數(shù)
當(dāng)在同一個(gè)函數(shù)funcA中存在M次調(diào)用底層操作函數(shù)Exec挖函,并且任意一次Exec調(diào)用可以重試R次時(shí)状植,這時(shí)打樁就需要用到Stub序列函數(shù)ExecSeqInject,代碼如下所示:
// test/infra-stub/oscap-stub/exec_stub.go
func ExecSeqInject(succOutputs []string, tryTimes int, err error) {
i := 0
length := 0
tryFailTimes := 0
needTry := false
if tryTimes == 0 {
length = len(succOutputs)
} else {
length = len(succOutputs) - 1
needTry = true
tryFailTimes = tryTimes - 1
}
osencap.Exec = func(cmd string, args ...string) (string, error) {
if i < length {
i++
return succOutputs[i - 1], nil
}
if needTry {
if tryFailTimes > 0 {
tryFailTimes--
return "", err
}
} else {
return "", err
}
return succOutputs[i], nil
}
}
對(duì)上面的代碼做些解釋:
- 當(dāng)tryTimes <= 0時(shí)挪圾,表示不重試浅萧,是普通的一次調(diào)用,該測(cè)試用例為錯(cuò)誤測(cè)試哲思;succOutputs是前面的len(succOutputs)次底層操作函數(shù)Exec正確調(diào)用的返回值切片
- 當(dāng)tryTimes > 0時(shí)洼畅,表示重試,重試的失敗次數(shù)為tryTimes - 1棚赔,最后一次重試成功帝簇,該測(cè)試為正確測(cè)試;succOutputs是前面的len(succOutputs) - 1次底層操作函數(shù)Exec正確調(diào)用的返回值與最后一次重試成功的返回值組成的切片
- 不管是否重試靠益,錯(cuò)誤時(shí)的返回值都是("", err)
測(cè)試funcA時(shí)丧肴,對(duì)函數(shù)Exec的打樁處理約定如下:
- 當(dāng)前N(0 < N < M)次調(diào)用都返回正確但第N + 1次調(diào)用不管有無(wú)重試都返回錯(cuò)誤時(shí),使用ExecSeqInject打樁
- 當(dāng)前N(0 < N < M)次調(diào)用都返回正確但第N + 1次調(diào)用有重試并且在最后一次重試時(shí)成功胧后,使用ExecSeqInject打樁
- 其他情況屬于簡(jiǎn)單場(chǎng)景芋浮,直接使用ExecInject打樁
Stub驗(yàn)證
我們共寫四個(gè)UT用例來(lái)驗(yàn)證Stub是否生效,前兩個(gè)用例針對(duì)Stub函數(shù)壳快,后兩個(gè)用例針對(duì)Stub序列函數(shù)纸巷,需要考慮原函數(shù)的備份和恢復(fù),即在stub前備份眶痰,在測(cè)試完成后恢復(fù)瘤旨。
第一個(gè)UT用例,測(cè)試正確情況下Stub函數(shù)是否成功注入竖伯,代碼如下所示:
func TestStubDemoForSucc(t *testing.T) {
backup := osencap.Exec
defer func() {
osencap.Exec = backup
}()
convey.Convey("stub demo for succ\n", t, func() {
outputExpect := "xxx-vethName100-yyy"
osencapstub.ExecInject(outputExpect, nil)
output, err := osencap.Exec(any, any)
convey.So(err, convey.ShouldEqual, nil)
convey.So(output, convey.ShouldEqual, outputExpect)
})
}
第二個(gè)UT用例存哲,測(cè)試錯(cuò)誤情況下Stub函數(shù)是否成功注入,錯(cuò)誤對(duì)象的個(gè)數(shù)決定了Stub注入的次數(shù)七婴,代碼如下所示:
const any = "any"
func TestStubDemoForFail(t *testing.T) {
backup := osencap.Exec
defer func() {
osencap.Exec = backup
}()
convey.Convey("stub demo for fail\n", t, func() {
osencapstub.ExecInject("", infra.ERR_EXEC_LOOKPATH_FAILED)
_, err := osencap.Exec(any, any)
convey.So(err, convey.ShouldEqual, infra.ERR_EXEC_LOOKPATH_FAILED)
osencapstub.ExecInject("", infra.ERR_EXEC_COMBINED_OUTPUT_FAILED)
_, err = osencap.Exec(any, any)
convey.So(err, convey.ShouldEqual, infra.ERR_EXEC_COMBINED_OUTPUT_FAILED)
})
}
第三個(gè)UT用例祟偷,測(cè)試Stub序列函數(shù)在錯(cuò)誤的場(chǎng)景是否成功注入,代碼如下所示:
func TestStubSeqDemoForFailAfter2Succ(t *testing.T) {
backup := osencap.Exec
defer func() {
osencap.Exec = backup
}()
convey.Convey("stub seq demo for fail after two times succ\n", t, func() {
outputsExpect := []string{"ok", "xxx-vethName100-yyy"}
osencapstub.ExecSeqInject(outputsExpect, 0, infra.ERR_EXEC_LOOKPATH_FAILED, )
output, err := osencap.Exec(any, any)
convey.So(err, convey.ShouldEqual, nil)
convey.So(output, convey.ShouldEqual, outputsExpect[0])
output, err = osencap.Exec(any, any)
convey.So(err, convey.ShouldEqual, nil)
convey.So(output, convey.ShouldEqual, outputsExpect[1])
_, err = osencap.Exec(any, any)
convey.So(err, convey.ShouldEqual, infra.ERR_EXEC_LOOKPATH_FAILED)
})
}
第四個(gè)UT用例打厘,測(cè)試Stub序列函數(shù)在正確的場(chǎng)景是否成功注入修肠,代碼如下所示:
func TestStubSeqDemoForSuccWithTryAfter2Succ(t *testing.T) {
backup := osencap.Exec
defer func() {
osencap.Exec = backup
}()
convey.Convey("stub seq demo for succ with try after two times succ\n", t, func() {
outputsExpect := []string{"ok", "xxx-vethName100-yyy", "success"}
maxTryTimes := 10
osencapstub.ExecSeqInject(outputsExpect, maxTryTimes, infra.ERR_EXEC_LOOKPATH_FAILED, )
output, err := osencap.Exec(any, any)
convey.So(err, convey.ShouldEqual, nil)
convey.So(output, convey.ShouldEqual, outputsExpect[0])
output, err = osencap.Exec(any, any)
convey.So(err, convey.ShouldEqual, nil)
convey.So(output, convey.ShouldEqual, outputsExpect[1])
for i := 0; i < maxTryTimes - 1; i++ {
_, err = osencap.Exec(any, any)
convey.So(err, convey.ShouldEqual, infra.ERR_EXEC_LOOKPATH_FAILED)
}
output, err = osencap.Exec(any, any)
convey.So(err, convey.ShouldEqual, nil)
convey.So(output, convey.ShouldEqual, outputsExpect[2])
})
}
小結(jié)
對(duì)于Golang來(lái)說(shuō),很多同學(xué)喜歡用GoMock打樁婚惫。不可否認(rèn)氛赐,GoMock非常優(yōu)秀,但對(duì)于底層的操作函數(shù)使用GoMock打樁會(huì)引入額外的復(fù)雜度先舷,因此筆者想嘗試其他方式艰管。本文借助閉包的特性對(duì)底層的操作函數(shù)進(jìn)行打樁,根據(jù)場(chǎng)景的不同將打樁函數(shù)分為Stub函數(shù)和Stub序列函數(shù)蒋川,簡(jiǎn)單實(shí)用牲芋,希望對(duì)讀者有一定的啟發(fā)。為了便于記憶和交流捺球,筆者將這種方法命名為Golang Stub缸浦,如有雷同,純屬巧合氮兵。