<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 16839字,需瀏覽 34分鐘

           ·

          2021-08-05 10:25

          單元測(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ù)庫連接、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è)試庫: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. 斷言庫:testify

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

          如果采用testify庫,需要引入"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 }

          更改為如下斷言形式

          1assert.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庫,則執(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依賴庫

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

          • go-sqlmock

          這是Go語言中用以測(cè)試數(shù)據(jù)庫交互的SQL模擬驅(qū)動(dòng)庫,其項(xiàng)目地址為 https://github.com/DATA-DOG/go-sqlmock。它而無需真正地?cái)?shù)據(jù)庫連接,就能夠在測(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ǔ)?。簃onkey 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庫通過在運(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庫的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è)試庫testing的快速生成測(cè)試代碼,斷言庫testify的斷言使用,接口mock框架gomock如何mock接口方法和一些常見的三方依賴mock庫推薦,最后再介紹了測(cè)試大殺器猴子補(bǔ)丁,當(dāng)然,不到萬不得已,不要使用猴子補(bǔ)丁。

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




          往期推薦



          機(jī)器鈴砍菜刀


          歡迎添加小菜刀微信

          加入Golang分享群學(xué)習(xí)交流!


          感謝你的點(diǎn)贊在看哦~


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

          手機(jī)掃一掃分享

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

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  好操逼网 | 色妺妺AV免费无码 | 国产蜜臀精品一区二区免费 | 黄色在线欣赏 | 欧洲草逼视频 |