Go語言如何在沒有實現功能的情況下寫出完善的單元測試代碼

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

背景

最近在研究用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 擷取更詳細的介面資訊。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.