Monkey框架使用指南
序言
要寫出好的測試程式碼,必須精通相關的測試框架。對於Golang的程式設計師來說,至少需要掌握下面四個測試框架:
GoConvey
GoStub
GoMock
Monkey
透過前面四篇文章,我們已經掌握了框架GoConvey + GoStub + GoMock組合使用的正確姿勢,同時已經知道:
全域性變數可透過GoStub框架打樁
過程可透過GoStub框架打樁
函式可透過GoStub框架打樁
interface可透過GoMock框架打樁
但還有兩個問題比較棘手:
方法(成員函式)無法透過GoStub框架打樁,當產品程式碼的OO設計比較多時,打樁點可能離被測函式比較遠,導致UT用例寫起來比較痛
過程或函式透過GoStub框架打樁時,對產品程式碼有侵入性
下面我們舉兩個例子,闡述GoStub框架對產品程式碼的侵入性
例一:函式定義侵入
func Exec(cmd string, args ...string) (string, error) { ... }
上面的函式Exec的定義為常規方式,但這時不能透過GoStub框架對函式Exec進行打樁,除非將函式Exec定義為非常規方式(侵入性):
var Exec = func(cmd string, args ...string) (string, error) { ... }
例二:適配層侵入
產品程式碼中很多函式都會呼叫Golang的庫函式或第三方的庫函式,這些庫函式的定義顯然是常規方式,要想透過GoStub框架對這些函式打樁,一般會在適配層定義相關的變數(侵入性):
package adaptervar Stat = os.Statvar Marshal = json.Marshalvar UnMarshal = json.Unmarshal ...
本文將介紹第四個框架Monkey的使用方法,目的是解決這兩個棘手的問題,同時考慮將GoStub的優點整合到Monkey。
Monkey簡介
Monkey是Golang的一個猴子補丁(monkeypatching)框架,在執行時透過彙編語句重寫可執行檔案,將待打樁函式或方法的實現跳轉到樁實現,原理和熱補丁類似。如果讀者想進一步瞭解Monkey的工作原理,請閱讀部落格:http://bouk.co/blog/monkey-patching-in-go/。
透過Monkey,我們可以解決函式或方法的打樁問題,但Monkey不是執行緒安全的,不要將Monkey用於併發的測試中。
安裝
在命令列執行下面的命令:
go get github.com/bouk/monkey
執行完後你會發現,在$GOPATH/src/github.com目錄下,新增了bouk/monkey子目錄,這就是本文的主角。
使用場景
Monkey框架的使用場景很多,依次為:
基本場景:為一個函式打樁
基本場景:為一個過程打樁
基本場景:為一個方法打樁
複合場景:由任意相同或不同的基本場景組合而成
特殊場景:樁中樁的一個案例
為一個函式打樁
Exec是infra層的一個操作函式,實現很簡單,程式碼如下所示:
// infra/os-encap/exec.gofunc Exec(cmd string, args ...string) (string, error) { cmdpath, err := exec.LookPath(cmd) if err != nil { fmt.Errorf("exec.LookPath err: %v, cmd: %s", err, cmd) return "", infra.ErrExecLookPathFailed } var output []byte output, err = exec.Command(cmdpath, args...).CombinedOutput() if err != nil { fmt.Errorf("exec.Command.CombinedOutput err: %v, cmd: %s", err, cmd) return "", infra.ErrExecCombinedOutputFailed } fmt.Println("CMD[", cmdpath, "]ARGS[", args, "]OUT[", string(output), "]") return string(output), nil}
Exec函式的實現中呼叫了庫函式exec.LoopPath和exec.Command,因此Exec函式的返回值和執行時的底層環境密切相關。在UT中,如果被測函式呼叫了Exec函式,則應根據用例的場景對Exec函式打樁。
Monkey的API非常簡單和直接,我們直接看打樁程式碼:
import ( "testing" . "github.com/smartystreets/goconvey/convey" . "github.com/bouk/monkey" "infra/osencap")const any = "any"func TestExec(t *testing.T) { Convey("test has digit", t, func() { Convey("for succ", func() { outputExpect := "xxx-vethName100-yyy" guard := Patch(osencap.Exec, func(_ string, _ ...string) (string, error) { return outputExpect, nil }) defer guard.Unpatch() output, err := osencap.Exec(any, any) So(output, ShouldEqual, outputExpect) So(err, ShouldBeNil) }) }) }
Patch是Monkey提供給使用者用於函式打樁的API:
第一個引數是目標函式的函式名
第二個引數是樁函式的函式名,習慣用法是匿名函式或閉包
返回值是一個PatchGuard物件指標,主要用於在測試結束時刪除當前的補丁
為一個過程打樁
當一個函式沒有返回值時,該函式我們一般稱為過程。很多時候,我們將資源清理類函式定義為過程。
我們對過程DestroyResource的打樁程式碼為:
guard := Patch(DestroyResource, func(_ string) { })defer guard.Unpatch()
為一個方法打樁
當微服務有多個例項時,先透過Etcd選舉一個Master例項,然後Master例項為所有例項較均勻的分配任務,並將任務分配結果Set到Etcd,最後Master和Node例項Watch到任務列表,並過濾出自身需要處理的任務列表。
我們用類Etcd的方法Get來模擬獲取任務列表的功能,入參為instanceId:
type Etcd struct { }func (e *Etcd) Get(instanceId string) []string { taskList := make([]string, 0) ... return taskList
我們對Get方法的打樁程式碼如下:
var e *Etcd guard := PatchInstanceMethod(reflect.TypeOf(e), "Get", func(_ *Etcd, _ string) []string { return []string{"task1", "task5", "task8"} })defer guard.Unpatch()
PatchInstanceMethod API是Monkey提供給使用者用於方法打樁的API:
在使用前,先要定義一個目標類的指標變數x
第一個引數是reflect.TypeOf(x)
第二個引數是字串形式的函式名
返回值是一個PatchGuard物件指標,主要用於在測試結束時刪除當前的補丁
任意相同或不同的基本場景組合
假設Px
為用於函式、過程或方法打樁的API呼叫,則任意相同或不同基本場景組合的打樁過程形式化表達為:
Px1defer UnpatchAll() Px2 ... Pxn
該測試執行完後,函式UnpatchAll
將刪除所有的補丁。
樁中樁的一個案例
在某些特殊場景下(比如反序列化),函式或方法既有返回值,又有出參。出參一般為指標型別,包括具體的指標型別(比如*int)和抽象的指標型別(一般為interface{})。我們常用的庫函式json.Unmarshal
就屬於這種情況。
筆者在實踐中遇到的出參型別大多是具體的指標型別,其指標變數指向的記憶體不管在傳入前確定還是在傳入後確定,都將影響後面的程式碼邏輯。
下面呈現樁中樁的一個案例,以便大家靈活使用Monkey框架。
何謂樁中樁?
interface中宣告瞭一個方法,既有返回值,又有出參。在測試中,先透過GoMock框架打樁多型到mock方法,然後又透過Monkey框架跳轉到補丁方法,最終修改出參並返回。在這個過程中,mock方法可以看作一個樁,補丁方法又可以看作mock方法的一個樁,即補丁方法是一個樁中樁。
定義一個具體型別Movie:
type Movie struct { Name string Type string Score int}
定義一個interface型別Repository:
type Repository interface { Retrieve(key string, movie *Movie) error ... }
樁中樁的一個測試用例:
func TestDemo(t *testing.T) { Convey("test demo", t, func() { Convey("retrieve movie", func() { ctrl := NewController(t) defer ctrl.Finish() mockRepo := mock_db.NewMockRepository(ctrl) mockRepo.EXPECT().Retrieve(Any(), Any()).Return(nil) Patch(redisrepo.GetInstance, func() Repository { return mockRepo }) defer UnpatchAll() PatchInstanceMethod(reflect.TypeOf(mockRepo), "Retrieve", func(_ *mock_db.MockRepository, name string, movie *Movie) error { movie = &Movie{Name: name, Type: "Love", Score: 95} return nil }) repo := redisrepo.GetInstance() var movie *Movie err := repo.Retrieve("Titanic", movie) So(err, ShouldBeNil) So(movie.Name, ShouldEqual, "Titanic") So(movie.Type, ShouldEqual, "Love") So(movie.Score, ShouldEqual, 95) }) ... }) }
我們先透過Monkey框架的Patch API將mock物件注入,然後透過Monkey框架的PatchInstanceMethod API將mock方法跳轉到補丁方法,間接完成對指標變數movie的記憶體分配及賦值,並返回nil。
Monkey的缺陷及解決方案
inline函式
Golang中雖然沒有inline關鍵字,但仍存在inline函式,一個函式是否是inline函式由編譯器決定。inline函式的特點是簡單短小,在原始碼的層次看有函式的結構,而在編譯後卻不具備函式的性質。inline函式不是在呼叫時發生控制轉移,而是在編譯時將函式體嵌入到每一個呼叫處,所以inline函式在呼叫時沒有地址。
inline函式沒有地址的特性導致了Monkey框架的第一個缺陷:對inline函式打樁無效。
模擬一個簡單的inline函式:
func IsEqual(a, b string) bool { return a == b }
對HasDigit函式進行打樁測試:
func TestIsEqual(t *testing.T) { Convey("test is equal", t, func() { Convey("for patch true", func() { guard := Patch(IsEqual, func(_, _ string) bool { return true }) defer guard.Unpatch() ok := IsEqual("hello", "world") So(ok, ShouldBeTrue) }) }) }
在命令列執行這個測試,結果不符合期望:
$ go test -v func_test.go -test.run TestIsEqual === RUN TestIsEqual test is equal for patch true Failures: * /Users/zhangxiaolong/Desktop/D/go-workspace/src/test/monkey/func_test.go Line 67: Expected: true Actual: false1 total assertion --- FAIL: TestIsEqual (0.00s) FAIL exit status 1FAIL command-line-arguments 0.006s
解決方案:透過命令列引數-gcflags=-l
禁止inline
在命令列增加引數-gcflags=-l
重新執行測試,結果符合期望:
go test -gcflags=-l -v func_test.go -test.run TestIsEqual === RUN TestIsEqual test is equal for patch true 1 total assertion --- PASS: TestIsEqual (0.00s) PASS ok command-line-arguments 0.007s
方法名首字母小寫
這一年多,Golang的版本在快速演進,上個月已經發布了go1.9版本。然而,一些團隊可能一直還在用go1.6版本,並有計劃在近期升級到go1.7或以上版本。
Monkey框架的實現中大量使用了反射機制,尤其是方法的補丁實現函式PatchInstanceMethod
。但是,go1.6版本和更高版本(比如go1.7)的反射機制有些差異:在go1.6版本中反射機制會匯出所有方法(不論首字母是大寫還是小寫),而在更高版本中反射機制僅會匯出首字母大寫的方法。
反射機制的這種差異導致了Monkey框架的第二個缺陷:在go1.6版本中可以成功打樁的首字母小寫的方法,當go版本升級後Monkey框架會顯式觸發panic,表示unknown method
:
m, ok := target.MethodByName(methodName)if !ok { panic(fmt.Sprintf("unknown method %s", methodName)) }
說明:反射機制的差異並不波及Patch
函式的實現,所以go版本升級前後首字母小寫的函式名的打樁不受影響。
正交設計四原則
告訴我們,要向穩定的方向依賴。首字母小寫的方法或函式不是public的,僅在包內可見,不是一個穩定的依賴方向。如果在UT測試中對首字母小寫的方法或函式打樁的話,會導致重構的成本比較大。
解決方案:不管現在團隊使用的go版本是哪一個,都不要對首字母小寫的方法或函式打樁,不但可以確保測試用例在go版本升級前後的穩定性,而且能有效降低重構的成本。
API
在討論Monkey的API之前,我們先回顧一下GoStub框架的API。
GoStub框架的API既包括函式API,也包括方法API。由於Monkey框架的API只涉及函式API,所以在這裡我們只回顧GoStub框架的函式API。
我們先看GoStub框架的第一個函式API:
func Stub(varToStub interface{}, stubVal interface{}) *Stubs
這個API我們一般用於對全域性變數打樁:
stubs := Stub(&num, 150)defer stubs.Reset()
然而,這個API也可以用於函式打樁:
stubs := Stub(&osencap.Exec, func(_ string, _ ...string) (string, error) { return "xxx-vethName100-yyy", nil})defer stubs.Reset()
GoStub框架的Stub API對函式的打樁方法是不是和Monkey框架的API的使用方法很像?這是毋庸置疑的,這樣的API才是原生的API,StubFunc API是專門針對函式或過程打樁的改進版:
func StubFunc(funcVarToStub interface{}, stubVal ...interface{}) *Stubs
StubFunc替代Stub對函式的打樁示例:
stubs := StubFunc(&osencap.Exec,"xxx-vethName100-yyy", nil)defer stubs.Reset()
是不是簡潔優雅了很多?
說明:一般情況下,Golang的樁函式都關注的是返回值,所以這種封裝很適用。但在特殊場景下,即樁函式在關注返回值的同時也關注出參,這時就要用原生的API。
為了應對多次呼叫樁函式而呈現不同行為
的複雜情況,筆者二次開發了GoStub框架,提供了下面的API:
type Values []interface{}type Output struct { StubVals Values Times int}func (s *Stubs) StubFuncSeq(funcVarToStub interface{}, outputs []Output) *Stubs
只有原生的API導致了Monkey框架的第三個缺陷:API不夠簡潔優雅,同時不支援多次呼叫樁函式(方法)而呈現不同行為的複雜情況。
解決方案:筆者計劃二次開發Monkey框架,增加下面四個API:
func PatchFunc(target interface{}, stubVal ...interface{}) *PatchGuardfunc PatchInstanceMethodFunc(target reflect.Type, methodName string, stubVal ...interface{}) *PatchGuardfunc PatchFuncSeq(target interface{}, outputs []Output) *PatchGuardfunc PatchInstanceMethodFuncSeq(target reflect.Type, methodName string, outputs []Output) *PatchGuard
小結
本文主要介紹了Monkey框架的使用方法,基本上解決了序言中提到的那兩個棘手的問題,同時針對Monkey框架的三個缺陷,分別提供瞭解決方案。
至此,我們已經知道:
全域性變數可透過GoStub框架打樁
過程可透過Monkey框架打樁
函式可透過Monkey框架打樁
方法可透過Monkey框架打樁
interface可透過GoMock框架打樁
我們在測試實踐中要舉一反三,深度掌握GoConvey + GoStub + GoMock + Monkey框架組合使用的正確姿勢,寫出高質量的測試程式碼。
我們在產品程式碼中,儘量不要使用全域性變數,同時筆者將會在近期完成對Monkey框架的二次開發。這樣的話,Monkey框架基本上就可以全部替代GoStub框架了,這或許就是一個守破離
的案例吧:)
當然,在Golang的UT測試實踐中,除過這幾個通用的測試框架,還有一些專用的測試框架需要掌握,比如和,讀者可根據實際需求自行學習。
作者:_張曉龍_
連結:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2730/viewspace-2810942/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- API資料加密框架monkey-api-encryptAPI加密框架
- Monkey命令
- Vue框架TypeScript裝飾器使用指南Vue框架TypeScript
- monkey操作命令
- Monkey二次開發 -- Monkey jar包構建JAR
- HamronyOS 自動化測試框架使用指南框架
- 猴爪 The Monkey's Paw
- monkey測試介紹
- Nuxt框架中內建元件詳解及使用指南(三)UX框架元件
- Nuxt框架中內建元件詳解及使用指南(二)UX框架元件
- Nuxt框架中內建元件詳解及使用指南(五)UX框架元件
- Nuxt框架中內建元件詳解及使用指南(四)UX框架元件
- Nuxt框架中內建元件詳解及使用指南(一)UX框架元件
- Monkey 測試方法和要求
- 2020-12-23Monkey使用
- Monkey基本用法與常用引數
- Android 自動化測試之 MonkeyAndroid
- 分散式快照:Monkey-Lamport協議分散式LAMP協議
- Monkey 01 lexer 詞法分析器詞法分析
- Monkey工具之fastbot-iOS實踐ASTiOS
- Android Monkey 壓力測試 介紹Android
- 淺談我對python中的monkey patchPython
- Library Monkey for Mac - 音訊控制整理工具Mac音訊
- Monkey 執行的時候,音樂播放..
- ESLint 使用指南EsLint
- SOAR 使用指南
- Rundeck使用指南
- gulp 使用指南
- FontAwesome使用指南
- ConstraintLayout使用指南AI
- git使用指南Git
- MacTeX 使用指南Mac
- SQLT 使用指南SQL
- Lombok使用指南Lombok
- CompletableFuture 使用指南
- Vim使用指南
- nmap使用指南
- GPG 使用指南