作者介紹:熊訓(xùn)德(英文名:Sundy),16年畢業(yè)于四川大學(xué)大學(xué)并加入騰訊。目前在騰訊云從事hadoop生態(tài)相關(guān)的云存儲(chǔ)和計(jì)算等后臺(tái)開發(fā),喜歡并專注于研究大數(shù)據(jù)、虛擬化和人工智能等相關(guān)技術(shù)。

本文檔說(shuō)明go語(yǔ)言自帶的測(cè)試框架未提供或者未方便地提供的測(cè)試方案,主要是用于解決寫單元測(cè)試中比較頭痛的依賴問(wèn)題。也就是偽造模式,經(jīng)典的偽造模式有樁對(duì)象(stub),模擬對(duì)象(mock)和偽對(duì)象(fake)。比較幸運(yùn)的是,社區(qū)有豐富的第三方測(cè)試框架支持支持。下面就對(duì)筆者親身試用并實(shí)踐到項(xiàng)目中的幾個(gè)框架做介紹:

1.gomock

https://godoc.org/github.com/golang/mock/gomock

gomock模擬對(duì)象的方式是讓用戶聲明一個(gè)接口,然后使用gomock提供的mockgen工具生成mock對(duì)象代碼。要模擬(mock)被測(cè)試代碼的依賴對(duì)象時(shí)候,即可使用mock出來(lái)的對(duì)象來(lái)模擬和記錄依賴對(duì)象的各種行為:比如最常用的返回值,調(diào)用次數(shù)等等。文字?jǐn)⑹鲇悬c(diǎn)抽象,直接上代碼:

dick.go中DickFunc依賴外部對(duì)象OutterObj,本示例就是說(shuō)明如何使用gomock框架控制所依賴的對(duì)象。

func DickFunc( outterObj MockInterface,para int)(result int){
    fmt.Println("This init DickFunc")
    fmt.Println("call outter.func:")    return outterObj.OutterFunc(para)
}

mockgen工具命令是:

mockgen -source {source_file}.go -destination {dest_file}.go

比如,本示例即是:

mockgen -source src_mock.go -destination dst_mock.go

執(zhí)行完后,可在同目錄下找到生成的dst_mock.go文件,可以看到mockgen工具也實(shí)現(xiàn)了接口:

接下來(lái)就可以使用mockgen工具生成的NewMockInterFace來(lái)生產(chǎn)mock對(duì)象,使用這個(gè)mock對(duì)象。OutterFunc()這個(gè)函數(shù),gomock在控制mock類時(shí)支持鏈?zhǔn)骄幊痰姆绞?,其原理和其他鏈?zhǔn)骄幊填愃埔恢本S持了一個(gè)Call對(duì)象,把需要控制的方法名,入?yún)ⅲ鰠?,調(diào)用次數(shù)以及前置和后置動(dòng)作等,最后使用反射來(lái)調(diào)用方法,所以這個(gè)Call對(duì)象是mock對(duì)象的代理。jmockit的早期版本也是jdk自帶的java.reflect.Proxy動(dòng)態(tài)代理實(shí)現(xiàn)的(最近的版本是動(dòng)態(tài)Instrumentation配合代理模式)。 

在本示例中只簡(jiǎn)單的更改了返回值,拋磚引玉:

func TestDickFunc(t *testing.T ){   mockCtrl := gomock.NewController(t)
//defer mockCtrl.Finish()

   mockObj := dick.NewMockMockInterface(mockCtrl)
   mockObj.EXPECT().OutterFunc(3).Return(10)

   result :=dick.DickFunc(mockObj,3)
   t.Log("resutl:",result)

}

使用go test命令執(zhí)行這個(gè)單測(cè) 

從結(jié)果看:本來(lái)應(yīng)該輸出3,最后輸出就是10,和其他語(yǔ)言mock框架相似,生產(chǎn)出來(lái)的Mock對(duì)象不用自己去重定義這么麻煩。

更多示例可以查看官網(wǎng)一個(gè)囊括gomock幾乎所有功能的例子:

https://godoc.org/github.com/golang/mock/sample

2.httpexcept

由于go在網(wǎng)絡(luò)架構(gòu)上的優(yōu)秀封裝,使得go在很多網(wǎng)絡(luò)場(chǎng)景被廣泛使用,而http協(xié)議是其中重要部分,在面對(duì)http請(qǐng)求的時(shí)候,可以對(duì)http的client進(jìn)行測(cè)試,算是mock的特殊應(yīng)用場(chǎng)景。

看一個(gè)簡(jiǎn)單的示例就輕松的看懂了:

func TestHttp(t *testing.T) {    handler := FruitServer()

    server := httptest.NewServer(handler)
    defer server.Close()

    e := httpexpect.New(t, server.URL)

    e.GET("/fruits").        Expect().        Status(http.StatusOK).JSON().Array().Empty()
}

其中還支持對(duì)不同方法(包括Header,Post等)的構(gòu)造以及返回值Json的自定義,更多細(xì)節(jié)查看其官網(wǎng)

3.testify

還有一個(gè)testify使用起來(lái)可以說(shuō)兼容了《一》中的gocheck和gomock,但是其mock使用稍微有點(diǎn)煩雜,使用繼承tetify.Mock(匿名組合)重新實(shí)現(xiàn)需要Mock的接口,在這個(gè)接口里使用者自己使用Called(反射實(shí)現(xiàn))被Mock的接口。

《單元測(cè)試的藝術(shù)》中認(rèn)為stub和mock最大的區(qū)別就依賴對(duì)象是否和被測(cè)對(duì)象有交互,而從結(jié)果看就是樁對(duì)象不會(huì)使測(cè)試失敗,它只是為被測(cè)對(duì)象提供依賴的對(duì)象,并不改變測(cè)試結(jié)果,而mock則會(huì)根據(jù)不同的交互測(cè)試要求,很可能會(huì)更改測(cè)試的結(jié)果。說(shuō)了這么多理論,但其實(shí)這兩種方法都不是割裂的,所以gomock框架除了像其名字一樣可以模擬對(duì)象以外,還提供了樁對(duì)象的功能(stub)。以其實(shí)現(xiàn)來(lái)說(shuō),更像是一個(gè)樁對(duì)象的注入。但是因?yàn)榧嫒萘硕鄠€(gè)有用的功能,所以其在社區(qū)最為火爆。

具體用法可參考其github主頁(yè)

4.go-sqlmock

還有一種比較常見(jiàn)的場(chǎng)景就是和數(shù)據(jù)庫(kù)的交互場(chǎng)景,go-sqlmock是sql模擬(Mock)驅(qū)動(dòng)器,主要用于測(cè)試數(shù)據(jù)庫(kù)的交互,go-sqlmock提供了完整的事務(wù)的執(zhí)行測(cè)試框架,最新的版本(16.11.02)還支持prepare參數(shù)化提交和執(zhí)行的Mock方案。

比如有這樣的被測(cè)函數(shù):

func recordStats(db *sql.DB, userID, productID int64) (err error) {
    tx, err := db.Begin()    if err != nil {        return
    }

    defer func() {        switch err {        case nil:
            err = tx.Commit()        default:
            tx.Rollback()
        }
    }()    if _, err = tx.Exec("UPDATE products SET views = views + 1"); err != nil {        return
    }    if _, err = tx.Exec("INSERT INTO product_viewers (user_id, product_id) VALUES (?, ?)", userID, productID); err != nil {        return
    }    return
}

func main() {

    db, err := sql.Open("mysql", "root@/root")    if err != nil {
        panic(err)
    }
    defer db.Close()    if err = recordStats(db, 1 , 5 ); err != nil {
        panic(err)
    }
}

單測(cè)時(shí):

func TestShouldUpdateStats(t *testing.T) {
    db, mock, err := sqlmock.New()    if err != nil {
        t.Fatalf("mock error: '%s' ", err)
    }
    defer db.Close()

    mock.ExpectBegin()
    mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
    mock.ExpectExec("INSERT INTO product_viewers")
          .WithArgs(2, 3)
          .WillReturnResult(sqlmock.NewResult(1, 1))
    mock.ExpectCommit()    if err = recordStats(db, 2, 3); err != nil {
        t.Errorf("exe error: %s", err)
    }    if err := mock.ExpectationsWereMet(); err != nil {
        t.Errorf("not implements: %s", err)
    }
}//測(cè)試回滾
func TestShouldRollbackStatUpdatesOnFailure(t *testing.T) {
    db, mock, err := sqlmock.New()    if err != nil {
        t.Fatalf("mock error: '%s'", err)
    }
    defer db.Close()

    mock.ExpectBegin()
    mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
    mock.ExpectExec("INSERT INTO product_viewers")
           .WithArgs(2, 3)
           .WillReturnError(fmt.Errorf("some error"))
    mock.ExpectRollback()    // 執(zhí)行被測(cè)方法,有錯(cuò)    if err = recordStats(db, 2, 3); err == nil {
        t.Errorf("not error")
    }    // 執(zhí)行被測(cè)方法,mock對(duì)象    if err := mock.ExpectationsWereMet(); err != nil {
        t.Errorf("not implements: %s", err)
    }
}

更多例子和詳情,請(qǐng)查看官網(wǎng):

https://github.com/DATA-DOG/go-sqlmock

介紹了這么多框架,最后需要說(shuō)明的也可能最重要的是寫代碼時(shí)就應(yīng)該考慮代碼是可被測(cè)試的。要使得單元測(cè)試容易寫,或者說(shuō)代碼容易被測(cè),其實(shí)很重要的一個(gè)部分就是被測(cè)代碼本身是容易被測(cè)的,也就是說(shuō)在設(shè)計(jì)和編寫代碼的時(shí)候就應(yīng)該先想到相好如何單元測(cè)試,甚至有人提出可以先寫單元測(cè)試,再寫具體被測(cè)代碼。因?yàn)橐粋€(gè)接口(或者稱為單元)在被設(shè)計(jì)好后,它實(shí)現(xiàn)就確定了,實(shí)際效果也確定了。這種方式被稱作測(cè)試驅(qū)動(dòng)開發(fā)(Test-Driven Development, TDD)。而對(duì)于已經(jīng)寫好的代碼,很大程度上不好測(cè)試,有一種方式是測(cè)試性重構(gòu),就是為了更好的測(cè)試而進(jìn)行重構(gòu)。這些一定程度上來(lái)說(shuō)并了解這些框架更重要,有意向可以,可以查閱有關(guān)兩本書《單元測(cè)試的藝術(shù)(第2版)》《xUnit測(cè)試模式》

參考: http://codethoughts.info/go/2015/04/05/how-to-test-go-code/

https://nathany.com/go-testing-toolbox/

http://shinley.com/index.html

《單元測(cè)試的藝術(shù)》

《xUnit測(cè)試模式》

相關(guān)閱讀:

go單元測(cè)試基本篇

【騰訊TMQ】敏捷測(cè)試-快速俘虜產(chǎn)品 & 開發(fā)

 


 

此文已由作者授權(quán)騰訊云技術(shù)社區(qū)發(fā)布,轉(zhuǎn)載請(qǐng)注明文章出處,獲取更多云計(jì)算技術(shù)干貨,可請(qǐng)前往騰訊云技術(shù)社區(qū)

原文鏈接https://www.qcloud.com/community/article/921985001483606833
歡迎大家關(guān)注騰訊云技術(shù)社區(qū)-博客園官方主頁(yè),我們將持續(xù)在博客園為大家推薦技術(shù)精品文章哦~

 

http://www.cnblogs.com/qcloud1001/p/6645461.html