這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
背景
最近在研究用Go寫一個自己的解釋型語言,有一本書叫《Writing An Interpreter In Go》, 作者在講解如何編寫解譯器的時候,都是從寫一個_test.go開始的,也就是說作者習慣於先寫單元測試,以測試驅動開發,其實這是一個非常好的習慣,不過,作者在寫_test.go檔案的時候,都是先假設這個結構體、函數已經存在了,並且沒有把關鍵的對象抽象成介面,因此,作者在運行go test的時候,是無法完成測試的,因為連編譯都過不了,必須一邊完善代碼,一邊重複運行go test,一直到完成開發。
基於這種開發模式下,其實我更期望能有一個Mock實現,寫測試代碼的時候暢通無阻,即使是沒有實現,也能把各個測試案例覆蓋到,當真實的實現完成後,我們只需要把mock實現替換成真實的實現就好了。
這麼做還帶來另一個好處,如果公司有SDET崗位,則可以直接讓測試人員編寫單元測試,開發工作單位和測試工作可以並行。
gomock 架構
昨天閑著沒事逛了逛 https://github.com/golang, 發現了一個非常有意思的架構: gomock, 官方的描述是,這是一個mocking framework, 在使用上也很簡單,大致的步驟如下:
1、定義一個待實現的介面.
type MyInterface interface { SomeMethod(x int64, y string) GetSomething() string }
2、使用mockgen產生mock代碼.
3、測試:
func TestMyThing(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() mockObj := something.NewMockMyInterface(mockCtrl) mockObj.EXPECT().SomeMethod(4, "blah") mockObj.EXPECT().GetSomething.Return("haha")}
看了這個步驟,想必大家應該猜到了,mocking framework 使用的是根據你的介面定義來自動產生一個Mock實現,我們還可以往這個實現裡注入資料。
期望介面調用參數
我們可以通過 .EXPECT() 為這個mock對象注入期望值
mockObj.EXPECT().SomeMethod(4, "blah")
mockObj.SomeMethod(4, "blah")
mockObj.SomeMethod(5, "bldah") // 此處調用時直接會拋出錯誤
對於參數類型的期望,在調用這個Mock函數的時候會直接拋錯異常
期望傳回值
.EXPECT() 同樣可以為某個函數注入傳回值
mockObj.EXPECT().GetSomething().Return("haha")
if "haha" == mockObj.GetSomething() { // -> 執行到這裡 // ...}// ...
if "haha" == mockObj.GetSomething() { // -> 不會執行到這裡 // ...}// ...
功能強的還不止這個,如果我們在測試一個迴圈,希望的是每次調用 GetSomething() 都返回不同的值,該怎麼辦?
答案很簡單,依次調用
gomock.InOrder( mockObj.EXPECT().GetSomething().Return("A"), mockObj.EXPECT().GetSomething().Return("B"), mockObj.EXPECT().GetSomething().Return("C"),)
接下來,讓我們來實戰一下吧。
gomock 實戰
我們以《Writing An Interpreter In Go》這本書中的 monkey 語言的 lexer 作為例子
我們看一下 monkey 的目錄結構:
monkey> tree ..├── lexer│ ├── lexer.go│ ├── lexer_test.go│ └── mock_lexer│ └── mock_lexer.go└── token └── token.go
Lexer和Token
type Lexer interface { NextToken() token.Token}
lexer的功能很簡單,每次調用NextToken(),都是返回下一個Token
Token的結構
type TokenType stringtype Token struct { Type TokenType // 類型 Literal string // 內容}
比如下面的go語句
var a = 1
lexter 在調用三次NextToken()後會得到三個Token, 依次是:
Token{VAR, var}Token{IDENT, a}Token{INT, 1}
測試思路
其實測試方法就是:給定一段代碼,用Lexer解析後,能得到指定順序的Token,而gomock是完全可以實現的。
使用gomock
安裝gomock
go get github.com/golang/mock/gomockgo get github.com/golang/mock/mockgen
產生mock代碼
mockgen -source lexer.go -destination mock_lexer/mock_lexer.go
編寫lexer_test.go
測試資料
input: 輸入的語句
tokens: 期望的Token
func getTestData() (input string, tokens []token.Token) { input = `let five = 5;let ten = 10;` tokens = []token.Token{ {token.LET, "let"}, {token.IDENT, "five"}, {token.ASSIGN, "="}, {token.INT, "5"}, {token.SEMICOLON, ";"}, {token.LET, "let"}, {token.IDENT, "ten"}, {token.ASSIGN, "="}, {token.INT, "10"}, {token.SEMICOLON, ";"}, {token.EOF, ""}, } return}
產生一個真實的MonkeyLexer執行個體
當然,這裡我們沒有實現,所以返回是nil
func newMonkeyLexer(input string, excepts []token.Token, t *testing.T) (l Lexer, deferFN func()) { return nil, func() {} // return NewMonkeyLexer(input), func() {}}
構建MockLexer執行個體
由於沒寫完真正的lexer, 那麼我們就開始Mock吧
func newMockLexer(input string, excepts []token.Token, t *testing.T) (l Lexer, deferFN func()) { ctrl := gomock.NewController(t) // 產生一個Mock執行個體 mockLexter := mock_lexer.NewMockLexer(ctrl) // 將期望值一次傳遞給 NextToken() // 每次調用 NextToken() 也會依次獲得期望值 for i := 0; i < len(excepts); i++ { mockLexter.EXPECT().NextToken().Return(excepts[i]) } l = mockLexter // 用於清理 deferFN = func() { ctrl.Finish() } return}
為了方便在mock執行個體和真實執行個體之間進行切換,我們可以通過環境變數來控制當前的測試執行個體是什麼,如果要使用mock進行測試,我們只需要在運行 go test 前執行:
> export GO_MOCK_TEST=1
或
> GO_MOCK_TEST=1 go test -v
func newLexer(input string, excepts []token.Token, t *testing.T) (l Lexer, deferFN func()) { env := os.Getenv("GO_MOCK_TEST") if env == "1" { t.Log("MOCK TEST ENABLED!!!") return newMockLexer(input, excepts, t) } return newMonkeyLexer(input, excepts, t)}
以下是真正的測試代碼,在沒真實實現monkey lexer的情況下,我們可以寫測試代碼了,而且如果運行 go test -v 也是能通過的。
func TestNextToken(t *testing.T) { input, excepts := getTestData() l, fn := newLexer(input, excepts, t) // 產生 Lexer 對象 defer fn() // 清理 for i, tt := range excepts { tok := l.NextToken() // 擷取下一個Token if tok.Type != tt.Type { t.Fatalf("tests[%d] - tokentype wrong. expected=%q, got=%q", i, tt.Type, tok.Type) } if tok.Literal != tt.Literal { t.Fatalf("tests[%d] - literal wrong. expected=%q, got=%q", i, tt.Literal, tok.Literal) } }}
下載完整的代碼:
https://github.com/xujinzheng/monkey
測試
使用mock執行個體進行測試
monkey> cd lexerlexer> GO_MOCK_TEST=1 go test -v=== RUN TestNextToken--- PASS: TestNextToken (0.00s)PASSok github.com/xujinzheng/monkey/lexer 0.007s
使用真實執行個體測試
func newMonkeyLexer(input string, excepts []token.Token, t *testing.T) (l Lexer, deferFN func()) { return NewMonkeyLexer(input), func() {}}
我們將測試資料修改一下,假設 ten=666, 但不修改期望值,讓測試報錯
input = `let five = 5;let ten = 666;`
再次運行測試
monkey> cd lexerlexer> go test -v=== RUN TestNextToken--- FAIL: TestNextToken (0.00s) lexer_test.go:52: tests[8] - literal wrong. expected="10", got="666"FAILexit status 1FAIL github.com/xujinzheng/monkey/lexer 0.008s
這裡就報錯了,說明我們的真實執行個體實現得有問題,需要修複這個BUG
gomock 的使用到這裡就結束了,除了上面介紹到的一些功能,gomock 還有很多其他豐富的方法,大家可以去 GoDoc 擷取更詳細的介面資訊。