1. 如何有效地測(cè)試Go代碼

        共 17061字,需瀏覽 35分鐘

         ·

        2021-02-28 20:37

        單元測(cè)試

        如果把開發(fā)程序比作蓋房子,那么我們必須確保所有的用料都是合格的,否則蓋起來的房子就會(huì)存在問題。對(duì)于程序而言,我們可以將蓋房子的磚頭、鋼筋、水泥等當(dāng)做一個(gè)個(gè)功能單元,如果每個(gè)單元是合格的,我們將有信心認(rèn)為程序是健壯的。單元測(cè)試(Unit Test,UT)就是檢驗(yàn)功能單元是否合格的工具。

        一個(gè)沒有UT的項(xiàng)目,它的代碼質(zhì)量與工程保證是堪憂的。但在實(shí)際開發(fā)工作中,很多程序員往往并不寫測(cè)試代碼,他們的開發(fā)周期可能如下圖所示。

        而做了充分UT的程序員,他們的項(xiàng)目開發(fā)周期更大概率如下。

        項(xiàng)目開發(fā)中,不寫UT也許能使代碼交付更快,但是我們無法保證寫出來的代碼真的能夠正確地執(zhí)行。寫UT可以減少后期解決bug的時(shí)間,也能讓我們放心地使用自己寫出來的代碼。從長(zhǎng)遠(yuǎn)來看,后者更能有效地節(jié)省開發(fā)時(shí)間。

        既然UT這么重要,是什么原因在阻止開發(fā)人員寫UT呢?這是因?yàn)槌碎_發(fā)人員的惰性習(xí)慣之外,編寫UT代碼同樣存在難點(diǎn)。

        1. 代碼耦合度高,缺少必要的抽象與拆分,以至于不知道如何寫UT。

        2. 存在第三方依賴,例如依賴數(shù)據(jù)庫(kù)連接、HTTP請(qǐng)求、數(shù)據(jù)緩存等。

        可見,編寫可測(cè)試代碼的難點(diǎn)就在于解耦依賴。


        接口與Mock


        對(duì)于難點(diǎn)1,我們需要面向接口編程。在《接口Interface——塑造健壯與可擴(kuò)展的Go應(yīng)用程序》一文中,我們討論了使用接口給代碼帶來的靈活解耦與高擴(kuò)展特性。接口是對(duì)一類對(duì)象的抽象性描述,表明該類對(duì)象能提供什么樣的服務(wù),它最主要的作用就是解耦調(diào)用者和實(shí)現(xiàn)者,這成為了可測(cè)試代碼的關(guān)鍵。

        對(duì)于難點(diǎn)2,我們可以通過Mock測(cè)試來解決。Mock測(cè)試就是在測(cè)試過程中,對(duì)于某些不容易構(gòu)造或者不容易獲取的對(duì)象,用一個(gè)虛擬的對(duì)象來創(chuàng)建以便測(cè)試的測(cè)試方法。

        如果我們的代碼都是面向接口編程,調(diào)用方與服務(wù)方將是松耦合的依賴關(guān)系。在測(cè)試代碼中,我們就可以Mock 出另一種接口的實(shí)現(xiàn),從而很容易地替換掉第三方的依賴。



        測(cè)試工具


        1. 自帶測(cè)試庫(kù):testing

        在介紹Mock測(cè)試之前,先看一下Go中最簡(jiǎn)單的測(cè)試單元應(yīng)該如何寫。假設(shè)我們?cè)?/span>math.go文件下有以下兩個(gè)函數(shù),現(xiàn)在我們需要對(duì)它們寫測(cè)試案例。

        1package math
        2
        3func Add(x, y int) int {
        4    return x + y
        5}
        6
        7func Multi(x, y int) int {
        8    return x * y
        9}

        如果我們的IDE是Goland,它有一個(gè)非常好用的一鍵測(cè)試代碼生成功能。

        如上圖所示,光標(biāo)置于函數(shù)名之上,右鍵選擇 Generate,我們可以選擇生成整個(gè)package、當(dāng)前file或者當(dāng)前選中函數(shù)的測(cè)試代碼。以 Tests for selection 為例,Goland 會(huì)自動(dòng)在當(dāng)前 math.go 同級(jí)目錄新建測(cè)試文件math_test.go,內(nèi)容如下。

         1package math
        2
        3import "testing"
        4
        5func TestAdd(t *testing.T) {
        6    type args struct {
        7        x int
        8        y int
        9    }
        10    tests := []struct {
        11        name string
        12        args args
        13        want int
        14    }{
        15        // TODO: Add test cases.
        16    }
        17    for _, tt := range tests {
        18        t.Run(tt.name, func(t *testing.T) {
        19            if got := Add(tt.args.x, tt.args.y); got != tt.want {
        20                t.Errorf("Add() = %v, want %v", got, tt.want)
        21            }
        22        })
        23    }
        24}

        可以看到,在Go測(cè)試慣例中,單元測(cè)試的默認(rèn)組織方式就是寫在以 _test.go 結(jié)尾的文件中,所有的測(cè)試方法也都是以 Test 開頭并且只接受一個(gè) testing.T 類型的參數(shù)。同時(shí),如果我們要給函數(shù)名為 Add 的方法寫單元測(cè)試,那么對(duì)應(yīng)的測(cè)試方法一般會(huì)被寫成 TestAdd 。

        當(dāng)測(cè)試模板生成之后,我們只需將測(cè)試案例添加至 TODO 即可。

         1        {
        2            "negative + negative",
        3            args{-1-1},
        4            -2,
        5        },
        6        {
        7            "negative + positive",
        8            args{-11},
        9            0,
        10        },
        11        {
        12            "positive + positive",
        13            args{11},
        14            2,
        15        },

        此時(shí),運(yùn)行測(cè)試文件,可以發(fā)現(xiàn)所有測(cè)試案例,均成功通過。

        1=== RUN   TestAdd
        2--- PASS: TestAdd (0.00s)
        3=== RUN   TestAdd/negative_+_negative
        4    --- PASS: TestAdd/negative_+_negative (0.00s)
        5=== RUN   TestAdd/negative_+_positive
        6    --- PASS: TestAdd/negative_+_positive (0.00s)
        7=== RUN   TestAdd/positive_+_positive
        8    --- PASS: TestAdd/positive_+_positive (0.00s)
        9PASS
        2. 斷言庫(kù):testify

        簡(jiǎn)單了解了Go內(nèi)置 testing 庫(kù)的測(cè)試寫法后,推薦一個(gè)好用的斷言測(cè)試庫(kù):testify。testify具有常見斷言和mock的工具鏈,最重要的是,它能夠與內(nèi)置庫(kù) testing 很好地配合使用,其項(xiàng)目地址位于https://github.com/stretchr/testify。

        如果采用testify庫(kù),需要引入"github.com/stretchr/testify/assert"。之外,上述測(cè)試代碼中以下部分

        1            if got := Add(tt.args.x, tt.args.y); got != tt.want {
        2                t.Errorf("Add() = %v, want %v", got, tt.want)
        3            }

        更改為如下斷言形式

        1     assert.Equal(t, Add(tt.args.x, tt.args.y), tt.want, tt.name)

        testify 提供的斷言方法幫助我們快速地對(duì)函數(shù)的返回值進(jìn)行測(cè)試,從而減少測(cè)試代碼工作量。它可斷言的類型非常豐富,例如斷言Equal、斷言NIl、斷言Type、斷言兩個(gè)指針是否指向同一對(duì)象、斷言包含、斷言子集等。

        不要小瞧這一行代碼,如果我們?cè)跍y(cè)試案例中,將"positive + positive"的期望值改為3,那么測(cè)試結(jié)果中會(huì)自動(dòng)提供報(bào)錯(cuò)信息。

         1...
        2=== RUN   TestAdd/positive_+_positive
        3    math_test.go:36
        4            Error Trace:    math_test.go:36
        5            Error:          Not equal: 
        6                            expected: 2
        7                            actual  : 3
        8            Test:           TestAdd/positive_+_positive
        9            Messages:       positive + positive
        10    --- FAIL: TestAdd/positive_+_positive (0.00s)
        11
        12
        13Expected :2
        14Actual   :3
        15...
        3. 接口mock框架:gomock

        介紹完基本的測(cè)試方法的寫法后,我們需要討論基于接口的 Mock 方法。在Go語言中,最通用的 Mock 手段是通過Go官方的 gomock 框架來自動(dòng)生成其 Mock 方法。該項(xiàng)目地址位于https://github.com/golang/mock。

        為了方便讀者理解,本文舉一個(gè)小明玩手機(jī)的例子。小明喜歡玩手機(jī),他每天都需要通過手機(jī)聊微信、玩王者、逛知乎,如果某天沒有干這些事情,小明就沒辦法睡覺。在該情景中,我們可以將手機(jī)抽象成接口如下。

        1// mockDemo/equipment/phone.go
        2type Phone interface {
        3    WeiXin() bool
        4    WangZhe() bool
        5    ZhiHu() bool
        6}

        小明手上有一部非常老的IPhone6s,我們?yōu)樵撌謾C(jī)對(duì)象實(shí)現(xiàn)Phone接口。

         1// mockDemo/equipment/phone6s.go
        2type Iphone6s struct {
        3}
        4
        5func NewIphone6s() *Iphone6s {
        6    return &Iphone6s{}
        7}
        8
        9func (p *Iphone6s) WeiXin() bool {
        10    fmt.Println("Iphone6s chat wei xin!")
        11    return true
        12}
        13
        14func (p *Iphone6s) WangZhe() bool {
        15    fmt.Println("Iphone6s play wang zhe!")
        16    return true
        17}
        18
        19func (p *Iphone6s) ZhiHu() bool {
        20    fmt.Println("Iphone6s read zhi hu!")
        21    return true
        22}

        接著,我們定義Person對(duì)象用來表示小明,并定義Person對(duì)象的生活函數(shù)dayLife和入睡函數(shù)goSleep。

         1// mockDemo/person.go
        2type Person struct {
        3    name  string
        4    phone equipment.Phone
        5}
        6
        7func NewPerson(name string, phone equipment.Phone) *Person {
        8    return &Person{
        9        name:  name,
        10        phone: phone,
        11    }
        12}
        13
        14func (x *Person) goSleep() {
        15    fmt.Printf("%s go to sleep!", x.name)
        16}
        17
        18func (x *Person) dayLife() bool {
        19    fmt.Printf("%s's daily life:\n", x.name)
        20    if x.phone.WeiXin() && x.phone.WangZhe() && x.phone.ZhiHu() {
        21        x.goSleep()
        22        return true
        23    }
        24    return false
        25}

        最后,我們把小明和iphone6s對(duì)象實(shí)例化出來,并開啟他一天的生活。

         1//mockDemo/main.go
        2func main() {
        3    phone := equipment.NewIphone6s()
        4    xiaoMing := NewPerson("xiaoMing", phone)
        5    xiaoMing.dayLife()
        6}
        7
        8// output
        9xiaoMing's daily life:
        10Iphone6s chat wei xin!
        11Iphone6s play wang zhe!
        12Iphone6s read zhi hu!
        13xiaoMing go to sleep!
        14

        由于小明每天必須刷完手機(jī)才能睡覺,即Person.goSleep,那么小明能否睡覺依賴于手機(jī)。

        按照當(dāng)前代碼,如果小明的手機(jī)壞了,或者小明換了一個(gè)手機(jī),那他就沒辦法睡覺了,這肯定是萬萬不行的。因此我們需要把小明對(duì)某特定手機(jī)的依賴Mock掉,這個(gè)時(shí)候 gomock 框架排上了用場(chǎng)。

        如果沒有下載gomock庫(kù),則執(zhí)行以下命令獲取

        1GO111MODULE=on go get github.com/golang/mock/mockgen

        通過執(zhí)行以下命令對(duì)phone.go中的Phone接口Mock

        1mockgen -destination equipment/mock_iphone.go -package equipment -source equipment/phone.go

        在執(zhí)行該命令前,當(dāng)前項(xiàng)目的組織結(jié)構(gòu)如下

        1.
        2├── equipment
        3│   ├── iphone6s.go
        4│   └── phone.go
        5├── go.mod
        6├── go.sum
        7├── main.go
        8└── person.go

        執(zhí)行mockgen命令之后,在equipment/phone.go的同級(jí)目錄,新生成了測(cè)試文件 mock_iphone.go(它的代碼自動(dòng)生成功能,是通過Go自帶generate工具完成的,感興趣的讀者可以閱讀《Go工具之generate》一文),其部分內(nèi)容如下

         1...
        2// MockPhone is a mock of Phone interface
        3type MockPhone struct {
        4    ctrl     *gomock.Controller
        5    recorder *MockPhoneMockRecorder
        6}
        7
        8// MockPhoneMockRecorder is the mock recorder for MockPhone
        9type MockPhoneMockRecorder struct {
        10    mock *MockPhone
        11}
        12
        13// NewMockPhone creates a new mock instance
        14func NewMockPhone(ctrl *gomock.Controller) *MockPhone {
        15    mock := &MockPhone{ctrl: ctrl}
        16    mock.recorder = &MockPhoneMockRecorder{mock}
        17    return mock
        18}
        19
        20// EXPECT returns an object that allows the caller to indicate expected use
        21func (m *MockPhone) EXPECT() *MockPhoneMockRecorder {
        22    return m.recorder
        23}
        24
        25// WeiXin mocks base method
        26func (m *MockPhone) WeiXin() bool {
        27    m.ctrl.T.Helper()
        28    ret := m.ctrl.Call(m, "WeiXin")
        29    ret0, _ := ret[0].(bool)
        30    return ret0
        31}
        32
        33// WeiXin indicates an expected call of WeiXin
        34func (mr *MockPhoneMockRecorder) WeiXin() *gomock.Call {
        35    mr.mock.ctrl.T.Helper()
        36    return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WeiXin", reflect.TypeOf((*MockPhone)(nil).WeiXin))
        37}
        38...

        此時(shí),我們的person.go中的 Person.dayLife 方法就可以測(cè)試了。

         1func TestPerson_dayLife(t *testing.T) {
        2    type fields struct {
        3        name  string
        4        phone equipment.Phone
        5    }
        6
        7  // 生成mockPhone對(duì)象
        8    mockCtl := gomock.NewController(t)
        9    mockPhone := equipment.NewMockPhone(mockCtl)
        10  // 設(shè)置mockPhone對(duì)象的接口方法返回值
        11    mockPhone.EXPECT().ZhiHu().Return(true)
        12    mockPhone.EXPECT().WeiXin().Return(true)
        13    mockPhone.EXPECT().WangZhe().Return(true)
        14
        15    tests := []struct {
        16        name   string
        17        fields fields
        18        want   bool
        19    }{
        20        {"case1", fields{"iphone6s", equipment.NewIphone6s()}, true},
        21        {"case2", fields{"mocked phone", mockPhone}, true},
        22    }
        23    for _, tt := range tests {
        24        t.Run(tt.name, func(t *testing.T) {
        25            x := &Person{
        26                name:  tt.fields.name,
        27                phone: tt.fields.phone,
        28            }
        29            assert.Equal(t, tt.want, x.dayLife())
        30        })
        31    }
        32}

        對(duì)接口進(jìn)行Mock,可以讓我們?cè)谖磳?shí)現(xiàn)具體對(duì)象的接口功能前,或者該接口調(diào)用代價(jià)非常高時(shí),也能對(duì)業(yè)務(wù)代碼進(jìn)行測(cè)試。而且在開發(fā)過程中,我們同樣可以利用Mock對(duì)象,不用因?yàn)榈却涌趯?shí)現(xiàn)方實(shí)現(xiàn)相關(guān)功能,從而停滯后續(xù)的開發(fā)。

        在這里我們能夠體會(huì)到在Go程序中接口對(duì)于測(cè)試的重要性。沒有接口的Go代碼,單元測(cè)試會(huì)非常難寫。所以,如果一個(gè)稍大型的項(xiàng)目中,沒有任何接口,那么該項(xiàng)目的質(zhì)量一定是堪憂的。

        4. 常見三方mock依賴庫(kù)

        在上文中提到,因?yàn)榇嬖谀承┐嬖诘谌揭蕾?,?huì)讓我們的代碼難以測(cè)試。但其實(shí)已經(jīng)有一些比較成熟的mock依賴庫(kù)可供我們使用。由于篇幅原因,以下列出的一些mock庫(kù)將不再貼出示例代碼,詳細(xì)信息可通過對(duì)應(yīng)的項(xiàng)目地址進(jìn)行了解。

        • go-sqlmock

        這是Go語言中用以測(cè)試數(shù)據(jù)庫(kù)交互的SQL模擬驅(qū)動(dòng)庫(kù),其項(xiàng)目地址為 https://github.com/DATA-DOG/go-sqlmock。它而無需真正地?cái)?shù)據(jù)庫(kù)連接,就能夠在測(cè)試中模擬sql驅(qū)動(dòng)程序行為,非常有助于維護(hù)測(cè)試驅(qū)動(dòng)開發(fā)(TDD)的工作流程。

        • httpmock

        用于模擬外部資源的http響應(yīng),它使用模式匹配的方式匹配 HTTP 請(qǐng)求的 URL,在匹配到特定的請(qǐng)求時(shí)就會(huì)返回預(yù)先設(shè)置好的響應(yīng)。其項(xiàng)目地址為 https://github.com/jarcoal/httpmock 。

        • gripmock

        它用于模擬gRPC服務(wù)的服務(wù)器,通過使用.proto文件生成對(duì)gRPC服務(wù)的實(shí)現(xiàn),其項(xiàng)目地址為 https://github.com/tokopedia/gripmock。

        • redismock

        用于測(cè)試與Redis服務(wù)器的交互,其項(xiàng)目地址位于 https://github.com/elliotchance/redismock。

        5. 猴子補(bǔ)丁:monkey patch

        如果上述的方案都不能很好的寫出測(cè)試代碼,這時(shí)可以考慮使用猴子補(bǔ)丁。猴子補(bǔ)丁簡(jiǎn)單而言就是屬性在運(yùn)行時(shí)的動(dòng)態(tài)替換,它在理論上可以替換運(yùn)行時(shí)中的一切函數(shù)。這種測(cè)試方式在動(dòng)態(tài)語言例如Python中比較合適。在Go中,monkey庫(kù)通過在運(yùn)行時(shí)重寫正在運(yùn)行的可執(zhí)行文件并插入跳轉(zhuǎn)到您要調(diào)用的函數(shù)來實(shí)現(xiàn)Monkey patching。項(xiàng)目作者寫道:這個(gè)操作很不安全,不建議任何人在測(cè)試環(huán)境之外進(jìn)行使用。其項(xiàng)目地址為https://github.com/bouk/monkey。

        monkey庫(kù)的API比較簡(jiǎn)單,例如可以通過調(diào)用 monkey.Patch(<target function>, <replacement function>)來實(shí)現(xiàn)對(duì)函數(shù)的替換,以下是操作示例。

         1package main
        2
        3import (
        4    "fmt"
        5    "os"
        6    "strings"
        7
        8    "bou.ke/monkey"
        9)
        10
        11func main() {
        12    monkey.Patch(fmt.Println, func(a ...interface{}) (n int, err error) {
        13        s := make([]interface{}, len(a))
        14        for i, v := range a {
        15            s[i] = strings.Replace(fmt.Sprint(v), "hell""*bleep*"-1)
        16        }
        17        return fmt.Fprintln(os.Stdout, s...)
        18    })
        19    fmt.Println("what the hell?"// what the *bleep*?
        20}

        需要注意的是,如果啟用了內(nèi)聯(lián),則monkey有時(shí)無法進(jìn)行patching,因此,我們需要嘗試在禁用內(nèi)聯(lián)的情況下運(yùn)行測(cè)試。例如以上例子,我們需要通過以下命令執(zhí)行。

        1$ go build -o main -gcflags=-l main.go;./main
        2what the *bleep*?


        總結(jié)


        在項(xiàng)目開發(fā)中,單元測(cè)試是重要且必須的。
        對(duì)于單元測(cè)試的兩大難點(diǎn):解耦依賴,我們的代碼可以采用 面向接口+mock依賴 的方式進(jìn)行組織,將依賴都做成可插拔的,那在單元測(cè)試?yán)锩娓綦x依賴就是一件水到渠成的事情。

        另外,本文討論了一些實(shí)用的測(cè)試具,包括自帶測(cè)試庫(kù)testing的快速生成測(cè)試代碼,斷言庫(kù)testify的斷言使用,接口mock框架gomock如何mock接口方法和一些常見的三方依賴mock庫(kù)推薦,最后再介紹了測(cè)試大殺器猴子補(bǔ)丁,當(dāng)然,不到萬不得已,不要使用猴子補(bǔ)丁。

        最后,在這些測(cè)試工具的使用上,本文的內(nèi)容也只是一些淺嘗輒止的介紹,希望讀者能夠在實(shí)際項(xiàng)目中多寫寫單元測(cè)試,深入體會(huì)TDD的開發(fā)思想。


        推薦閱讀


        福利

        我為大家整理了一份從入門到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進(jìn)階看什么。關(guān)注公眾號(hào) 「polarisxu」,回復(fù) ebook 獲??;還可以回復(fù)「進(jìn)群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。

        瀏覽 24
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 在线97 | 欧美精品一区二区三区使用方法 | 大型女浴室洗澡视频偷拍在线播放 | 男女操逼逼视频 | 娇妻被交换粗又大又硬动图 |