<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>

          白話 Golang 單元測(cè)試

          共 11840字,需瀏覽 24分鐘

           ·

          2022-06-27 23:39

          最近學(xué)習(xí)某個(gè) Golang 單元測(cè)試的課程,發(fā)現(xiàn)其中推薦使用 gomonkey[1] 這種黑科技,讓人略感意外,畢竟在軟件開(kāi)發(fā)領(lǐng)域,諸如依賴(lài)注入之類(lèi)的概念已經(jīng)流傳了幾十年了,本文希望通過(guò)一個(gè)例子的演化過(guò)程,來(lái)總結(jié)出 Golang 單元測(cè)試的最佳實(shí)戰(zhàn)。

          既然是白話,那么我們得想一個(gè)通俗易懂的例子,就拿普通人來(lái)說(shuō)吧:活著是為了什么,好好學(xué)習(xí),買(mǎi)房,結(jié)婚,任意一個(gè)環(huán)節(jié)出現(xiàn)意外,整個(gè)人生就會(huì)偏離軌道。下面我用 Golang 代碼來(lái)描述活著的過(guò)程,其中好好學(xué)習(xí),買(mǎi)房,結(jié)婚都可能受到不可控外界因素的影響,比如好好學(xué)習(xí)遇上教培跑路,買(mǎi)房遇上銀行限貸,結(jié)婚遇上彩禮漲價(jià)。

          下面問(wèn)題來(lái)了:請(qǐng)為「Live」編寫(xiě)單元測(cè)試,要求覆蓋率達(dá)到 100%。

          package main

          import (
           "errors"
           "math/rand"
          )

          // Live 活著
          func Live(money1, money2, money3 int64) error {
           if err := GoodGoodStudy(money1); err != nil {
            return err
           }
           if err := BuyHouse(money2); err != nil {
            return err
           }
           if err := Marry(money3); err != nil {
            return err
           }
           return nil
          }

          // GoodGoodStudy 好好學(xué)習(xí)
          func GoodGoodStudy(money int64) error {
           if rand.Intn(100) > 0 {
            return errors.New("error")
           }
           _ = money
           return nil
          }

          // BuyHouse 買(mǎi)房
          func BuyHouse(money int64) error {
           if rand.Intn(100) > 0 {
            return errors.New("error")
           }
           _ = money
           return nil
          }

          // Marry 結(jié)婚
          func Marry(money int64) error {
           if rand.Intn(100) > 0 {
            return errors.New("error")
           }
           _ = money
           return nil
          }

          既然單元測(cè)試要求達(dá)到 100% 的覆蓋率,那么我們就必須測(cè)試每一個(gè)可能的分支:

          • GoodGoodStudy 異常
          • GoodGoodStudy 正常;BuyHouse 異常
          • GoodGoodStudy 正常;BuyHouse 正常;Marry 異常
          • GoodGoodStudy 正常;BuyHouse 正常;Marry 正常

          第一版單元測(cè)試

          對(duì) Live 而言,GoodGoodStudy,BuyHouse 和 Marry 都屬于外部依賴(lài),通過(guò)使用 gomonkey,我們可以在運(yùn)行時(shí)動(dòng)態(tài)替換掉他們的實(shí)現(xiàn),從而確保流程進(jìn)入預(yù)定分支。在斷言部分我們使用了 testify[2],它比直接使用標(biāo)準(zhǔn)庫(kù)中的 testing[3] 包方便很多。

          package main

          import (
           "errors"
           "testing"

           "github.com/stretchr/testify/assert"

           . "github.com/agiledragon/gomonkey/v2"
          )

          func Test_Live1(t *testing.T) {
           patches := NewPatches()
           // GoodGoodStudy error
           patches.ApplyFunc(GoodGoodStudy, func(int64) error {
            return errors.New("error")
           })
           assert.Error(t, Live(100100100))
           patches.Reset()
           // BuyHouse error
           patches.ApplyFunc(GoodGoodStudy, func(int64) error {
            return nil
           })
           patches.ApplyFunc(BuyHouse, func(int64) error {
            return errors.New("error")
           })
           assert.Error(t, Live(100100100))
           patches.Reset()
           // Marry error
           patches.ApplyFunc(GoodGoodStudy, func(int64) error {
            return nil
           })
           patches.ApplyFunc(BuyHouse, func(int64) error {
            return nil
           })
           patches.ApplyFunc(Marry, func(int64) error {
            return errors.New("error")
           })
           assert.Error(t, Live(100100100))
           patches.Reset()
           // ok
           patches.ApplyFunc(GoodGoodStudy, func(int64) error {
            return nil
           })
           patches.ApplyFunc(BuyHouse, func(int64) error {
            return nil
           })
           patches.ApplyFunc(Marry, func(int64) error {
            return nil
           })
           assert.NoError(t, Live(100100100))
           patches.Reset()
          }

          第一版單元測(cè)試存在的問(wèn)題:原始代碼十幾行,單元測(cè)試代碼幾十行。在大話西游中,至尊寶在夢(mèng)中叫了晶晶的名字 98 次,叫了紫霞的名字 784 次。而在我們的單元測(cè)試中,GoodGoodStudy 正常的狀態(tài)寫(xiě)了三次,BuyHouse 正常的狀態(tài)寫(xiě)了兩次,雖然遠(yuǎn)比至尊寶重復(fù)的次數(shù)少,但重復(fù)始終是個(gè)壞味道。

          第二版單元測(cè)試

          通過(guò)使用 OutputCell,我們可以一次性控制多個(gè)狀態(tài)變化,從而去除重復(fù)的壞味道:

          package main

          import (
           "errors"
           "testing"

           "github.com/stretchr/testify/assert"

           . "github.com/agiledragon/gomonkey/v2"
          )

          func Test_Live2(t *testing.T) {
           patches := NewPatches()
           defer patches.Reset()
           output := []OutputCell{
            {Values: Params{errors.New("error")}, Times: 1},
            {Values: Params{nil}, Times: 3},
           }
           patches.ApplyFuncSeq(GoodGoodStudy, output)
           output = []OutputCell{
            {Values: Params{errors.New("error")}, Times: 1},
            {Values: Params{nil}, Times: 2},
           }
           patches.ApplyFuncSeq(BuyHouse, output)
           output = []OutputCell{
            {Values: Params{errors.New("error")}, Times: 1},
            {Values: Params{nil}, Times: 1},
           }
           patches.ApplyFuncSeq(Marry, output)
           // GoodGoodStudy error
           assert.Error(t, Live(100100100))
           // BuyHouse error
           assert.Error(t, Live(100100100))
           // Marry error
           assert.Error(t, Live(100100100))
           // ok
           assert.NoError(t, Live(100100100))
          }

          第二版單元測(cè)試存在的問(wèn)題:原始代碼邏輯中不同分支是有層次感的,瀏覽代碼的時(shí)候可以很自然的看出流程的走向,但是在單元測(cè)試代碼中,這種層次感消失了,如果不寫(xiě)注釋?zhuān)瑔渭兛磾嘌源a,那么我們很可能搞不清楚自己在干什么。

          第三版單元測(cè)試

          雖然 testify 的斷言很強(qiáng)大,但是在表達(dá)的層次感上卻是無(wú)力的,此時(shí)我們可以考慮用 goconvey[4] 取代 testfy,它支持嵌套,這正是我們想要得到的層次感。

          package main

          import (
           "errors"
           "testing"

           . "github.com/agiledragon/gomonkey/v2"
           . "github.com/smartystreets/goconvey/convey"
          )

          func Test_Live3(t *testing.T) {
           patches := NewPatches()
           defer patches.Reset()
           output := []OutputCell{
            {Values: Params{errors.New("error")}, Times: 1},
            {Values: Params{nil}, Times: 3},
           }
           patches.ApplyFuncSeq(GoodGoodStudy, output)
           output = []OutputCell{
            {Values: Params{errors.New("error")}, Times: 1},
            {Values: Params{nil}, Times: 2},
           }
           patches.ApplyFuncSeq(BuyHouse, output)
           output = []OutputCell{
            {Values: Params{errors.New("error")}, Times: 1},
            {Values: Params{nil}, Times: 1},
           }
           patches.ApplyFuncSeq(Marry, output)
           Convey("Live", t, func() {
            t.Log("LOG: Live")
            Convey("GoodGoodStudy error"func() {
             t.Log("LOG: GoodGoodStudy error")
             So(Live(100100100), ShouldBeError)
            })
            Convey("GoodGoodStudy ok"func() {
             t.Log("LOG: GoodGoodStudy ok")
             Convey("BuyHouse error"func() {
              t.Log("LOG: BuyHouse error")
              So(Live(100100100), ShouldBeError)
             })
             Convey("BuyHouse ok"func() {
              t.Log("LOG: BuyHouse ok")
              Convey("Marry error"func() {
               t.Log("LOG: Marry error")
               So(Live(100100100), ShouldBeError)
              })
              Convey("Marry ok"func() {
               t.Log("LOG: Marry ok")
               So(Live(100100100), ShouldBeNil)
              })
             })
            })
           })
          }

          補(bǔ)充說(shuō)明:如果你沒(méi)看過(guò) goconvey 的文檔,那么很可能會(huì)誤解其運(yùn)行機(jī)制,我在代碼里加了很多 t.Log,大家不妨猜猜它們的輸出順序是什么樣的。了解這一點(diǎn)對(duì)實(shí)現(xiàn) setup,teardown 很重要,篇幅所限,本文就不深入討論了,有興趣的朋友請(qǐng)自行查閱。

          第三版單元測(cè)試存在的問(wèn)題:雖然 gomonkey 可以通過(guò) OutputCell 一次性控制多個(gè)狀態(tài)變化,但是這些狀態(tài)卻是靜態(tài)的,被替換方法的參數(shù)和返回值沒(méi)有關(guān)聯(lián)。

          關(guān)于 Gomonkey 的原罪

          在單元測(cè)試領(lǐng)域,關(guān)于如何替換掉外部依賴(lài),主要有兩種技術(shù),分別是 mock 和 stub:mock 通過(guò)接口可以動(dòng)態(tài)調(diào)整外部依賴(lài)的返回值,而 stub 只能在運(yùn)行時(shí)靜態(tài)調(diào)整外部依賴(lài)的返回值,可以說(shuō) mock 包含了 stub,或者說(shuō) stub 是 mock 的子集,從本質(zhì)上講,gomonkey 屬于 stub 技術(shù),它存在諸多缺點(diǎn),比如:

          • 它違反了開(kāi)閉原則。
          • 運(yùn)行時(shí)必須關(guān)閉內(nèi)連「go test -gcflags=all=-l」。
          • 運(yùn)行時(shí)需要很高的權(quán)限,并且不同的硬件需要不同的黑科技[5]實(shí)現(xiàn)。

          對(duì) gomonkey 來(lái)說(shuō),我的看法很明確:雖然黑科技很神奇,但是能不用就不用!一旦發(fā)現(xiàn)不得不用,那么多半意味著你的代碼設(shè)計(jì)本身存在問(wèn)題。

          最終版單元測(cè)試

          很多人買(mǎi)電腦的時(shí)候?yàn)榱耸″X(qián)買(mǎi)了集成顯卡的電腦,結(jié)果等到需要換顯卡的時(shí)候才發(fā)現(xiàn)可拔插性的重要性,如果上天再給他們一次機(jī)會(huì),我猜他們一定會(huì)買(mǎi)獨(dú)立顯卡的電腦。

          Golang 崇尚接口,有了接口,我們就可以很自然的使用 mock 技術(shù),而不是 stub 技術(shù)。在這里,mock 就相當(dāng)于獨(dú)立顯卡,而 stub 就相當(dāng)于集成顯卡。

          下面讓我們通過(guò)接口重構(gòu)原始代碼,其中使用 gomock[6] 生成了 mock 對(duì)象:

          package main

          //go:generate mockgen -package main -source foo.go -destination=foo_mock.go

          // Life 人生
          type Life interface {
           // GoodGoodStudy 好好學(xué)習(xí)
           GoodGoodStudy(money int64) error
           // BuyHouse 買(mǎi)房
           BuyHouse(money int64) error
           // Marry 結(jié)婚
           Marry(money int64) error
          }

          // Person 普通人
          type Person struct {
           life Life
          }

          // Live 活著
          func (p *Person) Live(money1, money2, money3 int64) error {
           if err := p.life.GoodGoodStudy(money1); err != nil {
            return err
           }
           if err := p.life.BuyHouse(money2); err != nil {
            return err
           }
           if err := p.life.Marry(money3); err != nil {
            return err
           }
           return nil
          }

          有了 mock 對(duì)象以后,我們就好像置身在元宇宙中一樣,不再有 stub 的限制:

          package main

          import (
           "errors"
           "testing"

           gomock "github.com/golang/mock/gomock"

           . "github.com/smartystreets/goconvey/convey"
          )

          func Test_Live(t *testing.T) {
           ctrl := gomock.NewController(t)
           life := NewMockLife(ctrl)
           handler := func(money int64) error {
            if money <= 0 {
             return errors.New("error")
            }
            return nil
           }
           life.EXPECT().GoodGoodStudy(gomock.Any()).AnyTimes().DoAndReturn(handler)
           life.EXPECT().BuyHouse(gomock.Any()).AnyTimes().DoAndReturn(handler)
           life.EXPECT().Marry(gomock.Any()).AnyTimes().DoAndReturn(handler)
           Convey("Live", t, func() {
            person := &Person{
             life: life,
            }
            Convey("GoodGoodStudy error"func() {
             So(person.Live(0100100), ShouldBeError)
            })
            Convey("GoodGoodStudy ok"func() {
             Convey("BuyHouse error"func() {
              So(person.Live(1000100), ShouldBeError)
             })
             Convey("BuyHouse ok"func() {
              Convey("Marry error"func() {
               So(person.Live(1001000), ShouldBeError)
              })
              Convey("Marry ok"func() {
               So(person.Live(100100100), ShouldBeNil)
              })
             })
            })
           })
          }

          最后讓我們討論一下到底哪些依賴(lài)需要 mock,哪些不需要 mock。簡(jiǎn)單點(diǎn)說(shuō):所有可能出現(xiàn)不可控情況的依賴(lài)都需要 mock,這里的不可控主要分兩種:

          • 一種是運(yùn)行時(shí)間的不可控:比如一個(gè)高 CPU 任務(wù),單次執(zhí)行需要一分鐘,但是有一百個(gè)測(cè)試用例要跑,此時(shí)就需要 mock。
          • 一種是運(yùn)行結(jié)果的不可控:比如 mysql,redis 之類(lèi)的 IO 請(qǐng)求,雖然它們可能運(yùn)行的很快,但是因?yàn)榫W(wǎng)絡(luò)本身的限制有可能失敗,此時(shí)需要 mock。

          不過(guò) mock 雖好,但不要貪杯,千萬(wàn)不要手里拿著錘子,看哪都像釘子。舉個(gè)例子:Golang 里最流行的配置工具 Viper[7],其最常用的使用方式都是靜態(tài)調(diào)用,比如:「viper.GetXxx」,并沒(méi)有使用接口,自然 mock 也就無(wú)從談起,不過(guò)我們可以通過(guò)「viper.Set」很簡(jiǎn)單的替換方法的返回值,此時(shí) mock 與否也就不再重要了。

          參考資料

          [1]

          gomonkey: https://github.com/agiledragon/gomonkey

          [2]

          testify: https://github.com/stretchr/testify

          [3]

          testing: https://pkg.go.dev/testing

          [4]

          goconvey: https://github.com/smartystreets/goconvey

          [5]

          黑科技: https://github.com/agiledragon/gomonkey/releases/tag/v2.2.0

          [6]

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

          [7]

          Viper: https://github.com/spf13/viper



          推薦閱讀


          福利

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

          瀏覽 31
          點(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>
                  日韩午夜欧美精品 | 久热精品在线观看视频 | 学生妹看毛片 | 俺来了俺去了www色官网 | 免费毛片在线 |