這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
序言
要寫出好的測試代碼,必須精通相關的測試架構。對於Golang的程式員來說,至少需要掌握下面三個測試架構:
通過上一篇文章《GoConvey架構使用指南》的學習,大家熟悉了GoConvey架構的基本使用方法,雖然已經可以寫出簡單優雅的測試代碼,但是如果在被測函數中調用了底層操作函數,比如調用了os包的Stat函數,則需要在測試函數中先對該底層操作函數打樁。那麼,該如何對函數高效的打樁呢?
本文給大家介紹一款輕量級的GoStub架構,介面友好,可以對全域變數、函數或過程打樁,我們一起來體驗一下。
安裝
在命令列輸入命令:
go get github.com/prashantv/gostub
你會發現,在$GOPATH/src/github.com目錄下,新增了prashantv/gostub子目錄,這就是本文的主角。
使用情境
GoStub架構的使用情境很多,依次為:
- 基本情境:為一個全域變數打樁
- 基本情境:為一個函數打樁
- 基本情境:為一個過程打樁
- 複合情境:由任意相同或不同的基本情境組合而成
為一個全域變數打樁
假設num為被測函數中使用的一個全域整型變數,當前測試案例中假定num的值大於100,比如為150,則打樁的代碼如下:
stubs := Stub(&num, 150)defer stubs.Reset()
stubs是GoStub架構的函數介面Stub返回的對象,該對象有Reset操作,即將全域變數的值恢複為原值。
為一個函數打樁
假設我們產品的既有代碼中有下面的函數定義:
func Exec(cmd string, args ...string) (string, error) { ...}
則Exec函數是不能通過GoStub架構打樁的。
若要想對Exec函數通過GoStub架構打樁,則僅需對該函式宣告做很小的重構,即將Exec函數定義為匿名函數,同時將它賦值給Exec變數,重構後的代碼如下:
var Exec = func(cmd string, args ...string) (string, error) { ...}
說明:對於新增函數,請按上面的方式定義
當Exec函數重構成Exec變數後,絲毫不影響既有代碼中對Exec函數的調用。由於Exec變數是函數變數,所以我們一般將這類變數也叫做函數。
現在我們可以對Exec函數打樁了,代碼如下所示:
stubs := Stub(&Exec, func(cmd string, args ...string) (string, error) { return "xxx-vethName100-yyy", nil})defer stubs.Reset()
其實GoStub架構專門提供了StubFunc函數用於函數打樁,我們重構打樁代碼:
stubs := StubFunc(&Exec,"xxx-vethName100-yyy", nil)defer stubs.Reset()
產品代碼中很多函數都會調用Golang的庫函數或第三方的庫函數,我們又不能重構這些函數,那麼該如何對這些庫函數打樁?
答案很簡單,即在適配層中定義庫函數的變數,然後在產品代碼中使用該變數。
定義庫函數的變數:
package adaptervar Stat = os.Statvar Marshal = json.Marshalvar UnMarshal = json.Unmarshal...
使用UnMarshal的代碼:
bytes, err := adapter.Marshal(&student)if err != nil { ... return err}......
我們現在可以對庫函數進行打樁了。假設當前使用的庫函數為Marshal,因為Marshal函數有成功或失敗兩種情況,所以它有兩個樁函數,但對於每一個測試案例來說Unmarshal只有一個樁函數。
序列化成功時的打樁代碼為:
var liLei = `{"name":"LiLei", "age":"21"}`stubs := StubFunc(&adapter.Marshal, []byte(liLei), nil)defer stubs.Reset()
序列化失敗時的打樁代碼為:
stubs := StubFunc(&adapter.Marshal, nil, ERR_ANY)defer stubs.Reset()
為一個過程打樁
當一個函數沒有傳回值時,該函數我們一般稱為過程。很多時候,我們將資源清理類函數定義為過程。
我們對過程DestroyResource的打樁代碼為:
stubs := StubFunc(&DestroyResource)defer stubs.Reset()
任意相同或不同的原子情境的組合
不論是調用Stub函數還是StubFunc函數,都會產生一個stubs對象,該對象仍然有Stub方法和StubFunc方法,所以在一個測試案例中可以同時對多個全域變數、函數或過程打樁。這些全域變數、函數或過程會將初始值存在一個map中,並在延遲語句中通過Reset方法統一做復原處理。
假設Sf為Stub或StubFunc函數的調用,Sm為Stub或StubFunc方法的調用,則在一個測試案例中使用GoStub架構的打樁代碼為:
stubs := Sfdefer stubs.Reset()stubs.Sm1...stubs.SmN
不推薦將打樁代碼寫成下面的形式:
stubs := Sfdefer stubs.Sm1.(...).SmN.Reset()
TestFuncDemo
筆者在上一篇文章《GoConvey架構使用指南》中推薦讀者使用Convey語句的嵌套,即一個函數有一個測試函數,測試函數中嵌套兩級Convey語句,第一級Convey語句對應測試函數,第二級Convey語句對應測試案例。在第二級的每個Convey函數中都會產生一個stubs對象,彼此獨立,互不影響。
我們看一個針對GoStub架構使用的較為完整的測試函數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, "", ERR_ANY) //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, ERR_ANY) //several So assert }) })}
不適用的複雜情況
儘管GoStub架構已經可以優雅的解決很多情境的函數打樁問題,但對於一些複雜的情況,卻只能乾瞪眼:
被測函數中多次調用了資料庫讀操作函數介面 ReadDb,並且資料庫為key-value型。被測函數先是 ReadDb 了一個父目錄的值,然後在 for 迴圈中讀了若干個子目錄的值。在多個測試案例中都有將ReadDb打樁為在多次調用中呈現不同行為的需求,即父目錄的值不同於子目錄的值,並且子目錄的值也互不相等
被測函數中多次調用了同一底層操作函數,比如 exec.Command,函數參數既有命令也有命令參數。被測函數先是建立了一個對象,然後查詢對象的狀態,在對象狀態達不到期望時還要刪除對象。在多個測試案例中都有將 exec.Command 打樁為多次調用中呈現不同行為的需求,即建立對象、查詢對象狀態和刪除對象對傳回值的期望都不一樣
被測函數中多次調用了os.Stat 函數,以訪問多個不同目錄的檔案。在多個測試案例中都有將 os.Stat 打樁為多次調用中呈現不同行為的需求,即一個目錄下存放的檔案資訊期望是使用者資料,而另一個檔案存放的檔案資訊期望是平台資料
被測函數中有一個重要的操作函數,在失敗的情況下會進行若干次重試。在測重試的測試案例中有將該操作函數打樁為多次調用中呈現不同行為的需求,即前 n-1 次操作失敗,第n次操作成功
...
小結
GoStub是一款輕量級的測試架構,介面友好,可以對全域變數、函數或過程打樁。本文詳細闡述了GoStub架構的使用情境,並給出了一個較為完整的測試函數Demo,希望讀者能夠掌握GoStub架構的基本使用方法,提高單元測試水平,交付高品質的軟體。
針對GoStub架構不適用的複雜情況,筆者將該架構進行了二次開發,優雅的解決了問題,我們在下一篇文章中給出答案。