Golang Stub初體驗(yàn)

序言

對(duì)于領(lǐng)域?qū)ο蟮腢T測(cè)試來(lái)說(shuō)唱逢,基礎(chǔ)設(shè)施層(infra)的操作函數(shù)都應(yīng)該被打樁。對(duì)于Golang來(lái)說(shuō),大家通常會(huì)想到GoMock迂求。GoMock是由Golang官方開(kāi)發(fā)維護(hù)的針對(duì)Golang的Mock框架,代碼在github.com上托管狞甚。GoMock目前已經(jīng)實(shí)現(xiàn)了較為完整的基于interface的Mock功能锁摔,能夠與Golang內(nèi)建的testing包良好集成,使用GoMock編寫的測(cè)試文件哼审,能夠直接使用go test命令進(jìn)行測(cè)試谐腰,用戶API簡(jiǎn)單友好。

盡管GoMock非常優(yōu)秀涩盾,但是對(duì)于普通的函數(shù)打樁來(lái)說(shuō)也有一些缺點(diǎn):

  1. 必須引入額外的抽象(interface)
  2. 打樁過(guò)程比較重
  3. 既有代碼必須適配新增的抽象

我們知道十气,Golang支持閉包,這使得函數(shù)可以作為另一個(gè)函數(shù)的參數(shù)或返回值春霍,而且可以賦值給一個(gè)變量砸西。
閉包的特性使得筆者想到了Stub,于是開(kāi)始了本文的體驗(yàn)址儒。

Exec函數(shù)

Exec是infra層的一個(gè)操作函數(shù)芹枷,實(shí)現(xiàn)很簡(jiǎn)單,代碼如下所示:

func Exec(cmd string, args ...string) (string, error) {
    cmdpath, err := exec.LookPath(cmd)
    if err != nil {
        log.Errorf("exec.LookPath err: %v, cmd: %s", err, cmd)
        return "", infra.ERR_EXEC_LOOKPATH_FAILED
    }

    var output []byte
    output, err = exec.Command(cmdpath, args...).CombinedOutput()
    if err != nil {
        log.Errorf("exec.Command.CombinedOutput err: %v, cmd: %s", err, cmd)
        return "", infra.ERR_EXEC_COMBINED_OUTPUT_FAILED
    }
    log.Info("CMD[", cmdpath, "]ARGS[", args, "]OUT[", string(output), "]")
    return string(output), nil
}

Stub設(shè)計(jì)

物理設(shè)計(jì)

stub位于test目錄下莲趣,和*test目錄或文件并行為*stub鸳慈,比如
test/infra-test/os-encap-test/exec_test.go ==>
test/infra-stub/os-encap-stub/exec_stub.go

既有函數(shù)重構(gòu)

Exec函數(shù)的重構(gòu)非常簡(jiǎn)單,only and only:

  1. 在函數(shù)前面新增“var ="
  2. 將函數(shù)名Exec移動(dòng)到”=“前面
// infra/os-encap/exec.go
var Exec = func(cmd string, args ...string) (string, error) {
    cmdpath, err := exec.LookPath(cmd)
    if err != nil {
        log.Errorf("exec.LookPath err: %v, cmd: %s", err, cmd)
        return "", infra.ERR_EXEC_LOOKPATH_FAILED
    }

    var output []byte
    output, err = exec.Command(cmdpath, args...).CombinedOutput()
    if err != nil {
        log.Errorf("exec.Command.CombinedOutput err: %v, cmd: %s", err, cmd)
        return "", infra.ERR_EXEC_COMBINED_OUTPUT_FAILED
    }
    log.Info("CMD[", cmdpath, "]ARGS[", args, "]OUT[", string(output), "]")
    return string(output), nil
}

這樣簡(jiǎn)單的重構(gòu)后喧伞,還有一個(gè)大的優(yōu)勢(shì)是Exec函數(shù)的調(diào)用將保持不變走芋。

Stub函數(shù)

Exec的Stub函數(shù)我們命名為ExecInject绩郎,它的設(shè)計(jì)和實(shí)現(xiàn)非常簡(jiǎn)單,即ExecInject函數(shù)有多個(gè)入?yún)⑽坛眩瑳](méi)有返回值肋杖,而且入?yún)⒘斜砗虴xec函數(shù)的返回值列表一一對(duì)應(yīng),代碼如下所示:

// test/infra-stub/oscap-stub/exec_stub.go
func ExecInject(output string, err error) {
    osencap.Exec = func(cmd string, args ...string) (string, error) {
        return output, err
    }
}

Stub序列函數(shù)

當(dāng)在同一個(gè)函數(shù)funcA中存在M次調(diào)用底層操作函數(shù)Exec挖函,并且任意一次Exec調(diào)用可以重試R次時(shí)状植,這時(shí)打樁就需要用到Stub序列函數(shù)ExecSeqInject,代碼如下所示:

// test/infra-stub/oscap-stub/exec_stub.go
func ExecSeqInject(succOutputs []string, tryTimes int, err error) {
    i := 0
    length := 0
    tryFailTimes := 0
    needTry := false
    if tryTimes == 0 {
        length = len(succOutputs)
    } else {
        length = len(succOutputs) - 1
        needTry = true
        tryFailTimes = tryTimes - 1
    }

    osencap.Exec = func(cmd string, args ...string) (string, error) {
        if i < length {
            i++
            return succOutputs[i - 1], nil
        }
        if needTry {
            if tryFailTimes > 0 {
                tryFailTimes--
                return "", err
            }
        } else {
            return "", err
        }


        return succOutputs[i], nil
    }
}

對(duì)上面的代碼做些解釋:

  1. 當(dāng)tryTimes <= 0時(shí)挪圾,表示不重試浅萧,是普通的一次調(diào)用,該測(cè)試用例為錯(cuò)誤測(cè)試哲思;succOutputs是前面的len(succOutputs)次底層操作函數(shù)Exec正確調(diào)用的返回值切片
  2. 當(dāng)tryTimes > 0時(shí)洼畅,表示重試,重試的失敗次數(shù)為tryTimes - 1棚赔,最后一次重試成功帝簇,該測(cè)試為正確測(cè)試;succOutputs是前面的len(succOutputs) - 1次底層操作函數(shù)Exec正確調(diào)用的返回值與最后一次重試成功的返回值組成的切片
  3. 不管是否重試靠益,錯(cuò)誤時(shí)的返回值都是("", err)

測(cè)試funcA時(shí)丧肴,對(duì)函數(shù)Exec的打樁處理約定如下:

  1. 當(dāng)前N(0 < N < M)次調(diào)用都返回正確但第N + 1次調(diào)用不管有無(wú)重試都返回錯(cuò)誤時(shí),使用ExecSeqInject打樁
  2. 當(dāng)前N(0 < N < M)次調(diào)用都返回正確但第N + 1次調(diào)用有重試并且在最后一次重試時(shí)成功胧后,使用ExecSeqInject打樁
  3. 其他情況屬于簡(jiǎn)單場(chǎng)景芋浮,直接使用ExecInject打樁

Stub驗(yàn)證

我們共寫四個(gè)UT用例來(lái)驗(yàn)證Stub是否生效,前兩個(gè)用例針對(duì)Stub函數(shù)壳快,后兩個(gè)用例針對(duì)Stub序列函數(shù)纸巷,需要考慮原函數(shù)的備份和恢復(fù),即在stub前備份眶痰,在測(cè)試完成后恢復(fù)瘤旨。

第一個(gè)UT用例,測(cè)試正確情況下Stub函數(shù)是否成功注入竖伯,代碼如下所示:

func TestStubDemoForSucc(t *testing.T) {
    backup := osencap.Exec
    defer func() {
        osencap.Exec = backup
    }()

    convey.Convey("stub demo for succ\n", t, func() {
        outputExpect := "xxx-vethName100-yyy"
        osencapstub.ExecInject(outputExpect, nil)
        output, err := osencap.Exec(any, any)
        convey.So(err, convey.ShouldEqual, nil)
        convey.So(output, convey.ShouldEqual, outputExpect)
    })
}

第二個(gè)UT用例存哲,測(cè)試錯(cuò)誤情況下Stub函數(shù)是否成功注入,錯(cuò)誤對(duì)象的個(gè)數(shù)決定了Stub注入的次數(shù)七婴,代碼如下所示:

const any = "any"

func TestStubDemoForFail(t *testing.T) {
    backup := osencap.Exec
    defer func() {
        osencap.Exec = backup
    }()

    convey.Convey("stub demo for fail\n", t, func() {
        osencapstub.ExecInject("", infra.ERR_EXEC_LOOKPATH_FAILED)
        _, err := osencap.Exec(any, any)
        convey.So(err, convey.ShouldEqual, infra.ERR_EXEC_LOOKPATH_FAILED)

        osencapstub.ExecInject("", infra.ERR_EXEC_COMBINED_OUTPUT_FAILED)
        _, err = osencap.Exec(any, any)
        convey.So(err, convey.ShouldEqual, infra.ERR_EXEC_COMBINED_OUTPUT_FAILED)
    })
}

第三個(gè)UT用例祟偷,測(cè)試Stub序列函數(shù)在錯(cuò)誤的場(chǎng)景是否成功注入,代碼如下所示:

func TestStubSeqDemoForFailAfter2Succ(t *testing.T) {
    backup := osencap.Exec
    defer func() {
        osencap.Exec = backup
    }()

    convey.Convey("stub seq demo for fail after two times succ\n", t, func() {
        outputsExpect := []string{"ok", "xxx-vethName100-yyy"}
        osencapstub.ExecSeqInject(outputsExpect, 0, infra.ERR_EXEC_LOOKPATH_FAILED, )
        output, err := osencap.Exec(any, any)
        convey.So(err, convey.ShouldEqual, nil)
        convey.So(output, convey.ShouldEqual, outputsExpect[0])

        output, err = osencap.Exec(any, any)
        convey.So(err, convey.ShouldEqual, nil)
        convey.So(output, convey.ShouldEqual, outputsExpect[1])

        _, err = osencap.Exec(any, any)
        convey.So(err, convey.ShouldEqual, infra.ERR_EXEC_LOOKPATH_FAILED)
    })
}

第四個(gè)UT用例打厘,測(cè)試Stub序列函數(shù)在正確的場(chǎng)景是否成功注入修肠,代碼如下所示:

func TestStubSeqDemoForSuccWithTryAfter2Succ(t *testing.T) {
    backup := osencap.Exec
    defer func() {
        osencap.Exec = backup
    }()

    convey.Convey("stub seq demo for succ with try after two times succ\n", t, func() {
        outputsExpect := []string{"ok", "xxx-vethName100-yyy", "success"}
        maxTryTimes := 10
        osencapstub.ExecSeqInject(outputsExpect, maxTryTimes, infra.ERR_EXEC_LOOKPATH_FAILED, )
        output, err := osencap.Exec(any, any)
        convey.So(err, convey.ShouldEqual, nil)
        convey.So(output, convey.ShouldEqual, outputsExpect[0])

        output, err = osencap.Exec(any, any)
        convey.So(err, convey.ShouldEqual, nil)
        convey.So(output, convey.ShouldEqual, outputsExpect[1])

        for i := 0; i < maxTryTimes - 1; i++ {
            _, err = osencap.Exec(any, any)
            convey.So(err, convey.ShouldEqual, infra.ERR_EXEC_LOOKPATH_FAILED)
        }
        output, err = osencap.Exec(any, any)
        convey.So(err, convey.ShouldEqual, nil)
        convey.So(output, convey.ShouldEqual, outputsExpect[2])
    })
}

小結(jié)

對(duì)于Golang來(lái)說(shuō),很多同學(xué)喜歡用GoMock打樁婚惫。不可否認(rèn)氛赐,GoMock非常優(yōu)秀,但對(duì)于底層的操作函數(shù)使用GoMock打樁會(huì)引入額外的復(fù)雜度先舷,因此筆者想嘗試其他方式艰管。本文借助閉包的特性對(duì)底層的操作函數(shù)進(jìn)行打樁,根據(jù)場(chǎng)景的不同將打樁函數(shù)分為Stub函數(shù)和Stub序列函數(shù)蒋川,簡(jiǎn)單實(shí)用牲芋,希望對(duì)讀者有一定的啟發(fā)。為了便于記憶和交流捺球,筆者將這種方法命名為Golang Stub缸浦,如有雷同,純屬巧合氮兵。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末裂逐,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子泣栈,更是在濱河造成了極大的恐慌卜高,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,386評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件南片,死亡現(xiàn)場(chǎng)離奇詭異掺涛,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)疼进,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門薪缆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人伞广,你說(shuō)我怎么就攤上這事拣帽。” “怎么了赔癌?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,704評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵诞外,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我灾票,道長(zhǎng)峡谊,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,702評(píng)論 1 294
  • 正文 為了忘掉前任刊苍,我火速辦了婚禮既们,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘正什。我一直安慰自己啥纸,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布婴氮。 她就那樣靜靜地躺著斯棒,像睡著了一般盾致。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上荣暮,一...
    開(kāi)封第一講書(shū)人閱讀 51,573評(píng)論 1 305
  • 那天庭惜,我揣著相機(jī)與錄音,去河邊找鬼穗酥。 笑死护赊,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的砾跃。 我是一名探鬼主播骏啰,決...
    沈念sama閱讀 40,314評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼抽高!你這毒婦竟也來(lái)了判耕?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,230評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤翘骂,失蹤者是張志新(化名)和其女友劉穎祈秕,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體雏胃,經(jīng)...
    沈念sama閱讀 45,680評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡请毛,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了瞭亮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片方仿。...
    茶點(diǎn)故事閱讀 39,991評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖统翩,靈堂內(nèi)的尸體忽然破棺而出仙蚜,到底是詐尸還是另有隱情,我是刑警寧澤厂汗,帶...
    沈念sama閱讀 35,706評(píng)論 5 346
  • 正文 年R本政府宣布委粉,位于F島的核電站,受9級(jí)特大地震影響娶桦,放射性物質(zhì)發(fā)生泄漏贾节。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評(píng)論 3 330
  • 文/蒙蒙 一衷畦、第九天 我趴在偏房一處隱蔽的房頂上張望栗涂。 院中可真熱鬧,春花似錦祈争、人聲如沸斤程。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,910評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)忿墅。三九已至扁藕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間疚脐,已是汗流浹背纹磺。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,038評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留亮曹,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,158評(píng)論 3 370
  • 正文 我出身青樓秘症,卻偏偏與公主長(zhǎng)得像照卦,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子乡摹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理役耕,服務(wù)發(fā)現(xiàn),斷路器聪廉,智...
    卡卡羅2017閱讀 134,657評(píng)論 18 139
  • 能力模型 選擇題 [primary] 下面屬于關(guān)鍵字的是()A. funcB. defC. structD. cl...
    _張曉龍_閱讀 24,837評(píng)論 14 224
  • 序言 要寫出好的測(cè)試代碼瞬痘,必須精通相關(guān)的測(cè)試框架。對(duì)于Golang的程序員來(lái)說(shuō)板熊,至少需要掌握下面四個(gè)測(cè)試框架: G...
    _張曉龍_閱讀 23,718評(píng)論 7 43
  • 序言 要寫出好的測(cè)試代碼框全,必須精通相關(guān)的測(cè)試框架。對(duì)于Golang的程序員來(lái)說(shuō)干签,至少需要掌握下面四個(gè)測(cè)試框架: G...
    _張曉龍_閱讀 19,973評(píng)論 4 16
  • 序言 要寫出好的測(cè)試代碼津辩,必須精通相關(guān)的測(cè)試框架。對(duì)于Golang的程序員來(lái)說(shuō)容劳,至少需要掌握下面四個(gè)測(cè)試框架: G...
    _張曉龍_閱讀 4,167評(píng)論 1 13