序言
要寫出好的測(cè)試代碼作煌,必須精通相關(guān)的測(cè)試框架。對(duì)于Golang的程序員來說僧凤,至少需要掌握下面四個(gè)測(cè)試框架:
- GoConvey
- GoStub
- GoMock
- Monkey
通過上一篇文章《GoConvey框架使用指南》的學(xué)習(xí),大家熟悉了GoConvey框架的基本使用方法,雖然已經(jīng)可以寫出簡單優(yōu)雅的測(cè)試代碼芥映,但是如果在被測(cè)函數(shù)中調(diào)用了底層操作函數(shù),比如調(diào)用了os包的Stat函數(shù)远豺,則需要在測(cè)試函數(shù)中先對(duì)該底層操作函數(shù)打樁奈偏。那么,該如何對(duì)函數(shù)高效的打樁呢躯护?
本文給大家介紹一款輕量級(jí)的GoStub框架惊来,接口友好,可以對(duì)全局變量棺滞、函數(shù)或過程打樁裁蚁,我們一起來體驗(yàn)一下。
安裝
在命令行運(yùn)行命令:
go get github.com/prashantv/gostub
運(yùn)行完后你會(huì)發(fā)現(xiàn)继准,在$GOPATH/src/github.com目錄下枉证,新增了prashantv/gostub子目錄,這就是本文的主角移必。
使用場(chǎng)景
GoStub框架的使用場(chǎng)景很多室谚,依次為:
- 基本場(chǎng)景:為一個(gè)全局變量打樁
- 基本場(chǎng)景:為一個(gè)函數(shù)打樁
- 基本場(chǎng)景:為一個(gè)過程打樁
- 復(fù)合場(chǎng)景:由任意相同或不同的基本場(chǎng)景組合而成
為一個(gè)全局變量打樁
假設(shè)num為被測(cè)函數(shù)中使用的一個(gè)全局整型變量,當(dāng)前測(cè)試用例中假定num的值大于100,比如為150舞萄,則打樁的代碼如下:
stubs := Stub(&num, 150)
defer stubs.Reset()
stubs是GoStub框架的函數(shù)接口Stub返回的對(duì)象眨补,該對(duì)象有Reset操作管削,即將全局變量的值恢復(fù)為原值倒脓。
為一個(gè)函數(shù)打樁
假設(shè)我們產(chǎn)品的既有代碼中有下面的函數(shù)定義:
func Exec(cmd string, args ...string) (string, error) {
...
}
則Exec函數(shù)是不能通過GoStub框架打樁的。
若要想對(duì)Exec函數(shù)通過GoStub框架打樁含思,則僅需對(duì)該函數(shù)聲明做很小的重構(gòu)崎弃,即將Exec函數(shù)定義為匿名函數(shù),同時(shí)將它賦值給Exec變量含潘,重構(gòu)后的代碼如下:
var Exec = func(cmd string, args ...string) (string, error) {
...
}
說明:對(duì)于新增函數(shù)饲做,請(qǐng)按上面的方式定義
當(dāng)Exec函數(shù)重構(gòu)成Exec變量后,絲毫不影響既有代碼中對(duì)Exec函數(shù)的調(diào)用遏弱。由于Exec變量是函數(shù)變量盆均,所以我們一般將這類變量也叫做函數(shù)。
現(xiàn)在我們可以對(duì)Exec函數(shù)打樁了漱逸,代碼如下所示:
stubs := Stub(&Exec, func(cmd string, args ...string) (string, error) {
return "xxx-vethName100-yyy", nil
})
defer stubs.Reset()
其實(shí)GoStub框架專門提供了StubFunc函數(shù)用于函數(shù)打樁泪姨,我們重構(gòu)打樁代碼:
stubs := StubFunc(&Exec,"xxx-vethName100-yyy", nil)
defer stubs.Reset()
產(chǎn)品代碼中很多函數(shù)都會(huì)調(diào)用Golang的庫函數(shù)或第三方的庫函數(shù),我們又不能重構(gòu)這些函數(shù)饰抒,那么該如何對(duì)這些庫函數(shù)打樁肮砾?
答案很簡單,即在適配層中定義庫函數(shù)的變量袋坑,然后在產(chǎn)品代碼中使用該變量仗处。
定義庫函數(shù)的變量:
package adapter
var Stat = os.Stat
var Marshal = json.Marshal
var UnMarshal = json.Unmarshal
...
使用UnMarshal的代碼:
bytes, err := adapter.Marshal(&student)
if err != nil {
...
return err
}
...
...
我們現(xiàn)在可以對(duì)庫函數(shù)進(jìn)行打樁了。假設(shè)當(dāng)前使用的庫函數(shù)為Marshal枣宫,因?yàn)镸arshal函數(shù)有成功或失敗兩種情況婆誓,所以它有兩個(gè)樁函數(shù),但對(duì)于每一個(gè)測(cè)試用例來說Unmarshal只有一個(gè)樁函數(shù)也颤。
序列化成功時(shí)的打樁代碼為:
var liLei = `{"name":"LiLei", "age":"21"}`
stubs := StubFunc(&adapter.Marshal, []byte(liLei), nil)
defer stubs.Reset()
序列化失敗時(shí)的打樁代碼為:
stubs := StubFunc(&adapter.Marshal, nil, ErrAny)
defer stubs.Reset()
為一個(gè)過程打樁
當(dāng)一個(gè)函數(shù)沒有返回值時(shí)洋幻,該函數(shù)我們一般稱為過程。很多時(shí)候歇拆,我們將資源清理類函數(shù)定義為過程鞋屈。
我們對(duì)過程DestroyResource的打樁代碼為:
stubs := StubFunc(&DestroyResource)
defer stubs.Reset()
任意相同或不同的原子場(chǎng)景的組合
不論是調(diào)用Stub函數(shù)還是StubFunc函數(shù),都會(huì)生成一個(gè)stubs對(duì)象故觅,該對(duì)象仍然有Stub方法和StubFunc方法厂庇,所以在一個(gè)測(cè)試用例中可以同時(shí)對(duì)多個(gè)全局變量、函數(shù)或過程打樁输吏。這些全局變量权旷、函數(shù)或過程會(huì)將初始值存在一個(gè)map中,并在延遲語句中通過Reset方法統(tǒng)一做回滾處理。
假設(shè)Sf為Stub或StubFunc函數(shù)的調(diào)用拄氯,Sm為Stub或StubFunc方法的調(diào)用躲查,則在一個(gè)測(cè)試用例中使用GoStub框架的打樁代碼為:
stubs := Sf
defer stubs.Reset()
stubs.Sm1
...
stubs.SmN
不推薦將打樁代碼寫成下面的形式:
stubs := Sf
defer stubs.Sm1.(...).SmN.Reset()
TestFuncDemo
筆者在上一篇文章《GoConvey框架使用指南》中推薦讀者使用Convey語句的嵌套,即一個(gè)函數(shù)有一個(gè)測(cè)試函數(shù)译柏,測(cè)試函數(shù)中嵌套兩級(jí)Convey語句镣煮,第一級(jí)Convey語句對(duì)應(yīng)測(cè)試函數(shù),第二級(jí)Convey語句對(duì)應(yīng)測(cè)試用例鄙麦。在第二級(jí)的每個(gè)Convey函數(shù)中都會(huì)產(chǎn)生一個(gè)stubs對(duì)象典唇,彼此獨(dú)立,互不影響胯府。
我們看一個(gè)針對(duì)GoStub框架使用的較為完整的測(cè)試函數(shù)Demo:
func TestFuncDemo(t *testing.T) {
Convey("TestFuncDemo", t, func() {
Convey("for succ", func() {
stubs := Stub(&num, 150)
defer stubs.Reset()
stubs.StubFunc(&Exec,"xxx-vethName100-yyy", nil)
var liLei = `{"name":"LiLei", "age":"21"}`
stubs.StubFunc(&adapter.Marshal, []byte(liLei), nil)
stubs.StubFunc(&DestroyResource)
//several So assert
})
Convey("for fail when num is too small", func() {
stubs := Stub(&num, 50)
defer stubs.Reset()
//several So assert
})
Convey("for fail when Exec error", func() {
stubs := Stub(&num, 150)
defer stubs.Reset()
stubs.StubFunc(&Exec, "", ErrAny)
//several So assert
})
Convey("for fail when Marshal error", func() {
stubs := Stub(&num, 150)
defer stubs.Reset()
stubs.StubFunc(&Exec,"xxx-vethName100-yyy", nil)
stubs.StubFunc(&adapter.Marshal, nil, ErrAny)
//several So assert
})
})
}
不適用的復(fù)雜情況
盡管GoStub框架已經(jīng)可以優(yōu)雅的解決很多場(chǎng)景的函數(shù)打樁問題介衔,但對(duì)于一些復(fù)雜的情況,卻只能干瞪眼:
被測(cè)函數(shù)中多次調(diào)用了數(shù)據(jù)庫讀操作函數(shù)接口 ReadDb骂因,并且數(shù)據(jù)庫為key-value型炎咖。被測(cè)函數(shù)先是 ReadDb 了一個(gè)父目錄的值,然后在 for 循環(huán)中讀了若干個(gè)子目錄的值寒波。在多個(gè)測(cè)試用例中都有將ReadDb打樁為在多次調(diào)用中呈現(xiàn)不同行為的需求乘盼,即父目錄的值不同于子目錄的值,并且子目錄的值也互不相等
被測(cè)函數(shù)中有一個(gè)循環(huán)影所,用于一個(gè)批量操作蹦肴,當(dāng)某一次操作失敗,則返回失敗猴娩,并進(jìn)行錯(cuò)誤處理阴幌。假設(shè)該操作為Apply,則在異常的測(cè)試用例中有將Apply打樁為在多次調(diào)用中呈現(xiàn)不同行為的需求卷中,即Apply的前幾次調(diào)用返回成功但最后一次調(diào)用卻返回失敗
被測(cè)函數(shù)中多次調(diào)用了同一底層操作函數(shù)矛双,比如 exec.Command,函數(shù)參數(shù)既有命令也有命令參數(shù)蟆豫。被測(cè)函數(shù)先是創(chuàng)建了一個(gè)對(duì)象议忽,然后查詢對(duì)象的狀態(tài),在對(duì)象狀態(tài)達(dá)不到期望時(shí)還要?jiǎng)h除對(duì)象十减,其中查詢對(duì)象是一個(gè)重要的操作栈幸,一般會(huì)進(jìn)行多次重試。在多個(gè)測(cè)試用例中都有將 exec.Command 打樁為多次調(diào)用中呈現(xiàn)不同行為的需求帮辟,即創(chuàng)建對(duì)象速址、查詢對(duì)象狀態(tài)和刪除對(duì)象對(duì)返回值的期望都不一樣
...
小結(jié)
GoStub是一款輕量級(jí)的測(cè)試框架,接口友好由驹,可以對(duì)全局變量芍锚、函數(shù)或過程打樁。本文詳細(xì)闡述了GoStub框架的使用場(chǎng)景,并給出了一個(gè)較為完整的測(cè)試函數(shù)Demo并炮,希望讀者能夠掌握GoStub框架的基本使用方法默刚,提高單元測(cè)試水平,交付高質(zhì)量的軟件逃魄。
針對(duì)GoStub框架不適用的復(fù)雜情況荤西,筆者將該框架進(jìn)行了二次開發(fā),優(yōu)雅的解決了問題嗅钻,我們?cè)谙乱黄恼轮薪o出答案皂冰。