go單元測(cè)試工具--mockey

前言

越來(lái)越覺(jué)得編寫單元測(cè)試是程序員的基本素養(yǎng)

之前寫單元測(cè)試都是基于go自己的test方式,基本就是在線下跑通流程涩嚣,遇到下游的接口無(wú)法訪問(wèn)時(shí),只會(huì)束手無(wú)策。后來(lái)了解到一些單測(cè)工具滋觉,仿佛打開了新世界的大門。市面上已有很多成熟的單測(cè)工具齐邦,本文不會(huì)比較各種工具的優(yōu)劣椎侠,而是結(jié)合自身經(jīng)驗(yàn)介紹筆者使用的幾款工具。

我們?cè)谧珜憜卧獪y(cè)試的過(guò)程中其實(shí)關(guān)注的主要是兩部分內(nèi)容:斷言和mock措拇。

斷言

斷言(assertion)是一種在程序中的一階邏輯(如:一個(gè)結(jié)果為真或假的邏輯判斷式)我纪,目的為了表示與驗(yàn)證軟件開發(fā)者預(yù)期的結(jié)果——當(dāng)程序執(zhí)行到斷言的位置時(shí),對(duì)應(yīng)的斷言應(yīng)該為真丐吓。 若斷言不為真時(shí)浅悉,程序會(huì)中止執(zhí)行,并給出錯(cuò)誤信息券犁。

說(shuō)白了斷言就是判斷某個(gè)結(jié)果是否符合預(yù)期术健,這里比較推薦goconvey

比如針對(duì)以下方法,我們可以編寫相關(guān)的單元測(cè)試用例如下

func Add(a, b int) int {
    return a + b
}
import (
    "testing"

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

func TestAdd(t *testing.T) {
    PatchConvey("test Add", t, func() {
        PatchConvey("test 2+3=5", func() {
            sum := Add(2, 3)
            So(sum, ShouldEqual, 5)
        })

        PatchConvey("test 1+1 != 3", func() {
            sum := Add(1, 1)
            So(sum, ShouldNotEqual, 3)
        })
    })
}

從上述例子中我們可以看到粘衬,goconvey提供了Convey方法幫助我們進(jìn)行測(cè)試用例的組織和編排苛坚,他能支持多級(jí)嵌套,方便我們進(jìn)行case管理色难。

運(yùn)行單元測(cè)試后泼舱,我們可以看到相關(guān)代碼的覆蓋率,這樣可進(jìn)一步幫助我們判斷單測(cè)覆蓋情況枷莉,查漏補(bǔ)缺娇昙。

mock

在單元測(cè)試中,模擬對(duì)象可以模擬復(fù)雜的笤妙、真實(shí)的(非模擬)對(duì)象的行為冒掌, 如果真實(shí)的對(duì)象無(wú)法放入單元測(cè)試中噪裕,使用模擬對(duì)象就很有幫助。

當(dāng)我們的代碼依賴較多股毫,由于多種因素導(dǎo)致我們可能準(zhǔn)確的控制這些依賴的返回值膳音,比如你在線下環(huán)境測(cè)試,依賴的某些服務(wù)并沒(méi)有部署線下環(huán)境铃诬,此時(shí)你的代碼根本無(wú)法執(zhí)行通過(guò)祭陷;如果直接在預(yù)覽環(huán)境測(cè)試有可能導(dǎo)致線上風(fēng)險(xiǎn),因此這時(shí)候我們就需要對(duì)這些下游服務(wù)的返回結(jié)果進(jìn)行mock(關(guān)于mock工具比較推薦字節(jié)的mockey)趣席,使其按照我們預(yù)期的結(jié)果進(jìn)行返回兵志。

此處的下游不一定就是外部的服務(wù),也可能是自身的方法或者函數(shù)宣肚。根據(jù)工作中實(shí)際場(chǎng)景想罕,將mock分為如下幾類:

mock對(duì)象方法

type Animal struct {}

func (t*Animal)Run() string {
    return "animal run"
}

func AnimalRun() string {
    animal := &Animal{}
    return animal.Run()
}
func TestAnimalRun(t *testing.T) {
    PatchConvey("test animal run", t, func() {
        Mock((*Animal).Run).Return("animal jump").Build()
        So(AnimalRun(), ShouldEqual, "animal jump")
    })
}

我們通過(guò)Mock方法修改了Add函數(shù)的返回值恒定為10。注意:Return()方法中參數(shù)的數(shù)量要與被mock函數(shù)的返回值數(shù)量及其順序保持一致霉涨。

mock函數(shù)

func Add(a, b int) int {
    return a + b
}

func TwoSum(a, b int) int {
    return Add(a, b)
}
import (
    "testing"

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

func TestTwoSum(t *testing.T) {
    PatchConvey("test two sum", t, func() {
        Mock(Add).Return(10).Build()
        So(TwoSum(1, 2), ShouldEqual, 10)
    })
}

序列化mock

在實(shí)際工作中會(huì)有這樣一種場(chǎng)景按价,我們會(huì)會(huì)在一次請(qǐng)求處理中對(duì)某個(gè)方法調(diào)用多次,我們希望每次調(diào)用都可以返回不同的結(jié)果笙瑟,這種該如何實(shí)現(xiàn)呢俘枫?別擔(dān)心,mockey提供了序列化方式逮走,可以統(tǒng)mock函數(shù)在多次執(zhí)行中每次執(zhí)行的結(jié)果鸠蚪,我們看下如何示例:

type Event struct {
    Extra string `json:"extra"` // map
}

func parseEvent(value string) (map[string]interface{},error) {
    event := &Event{}
    if err := json.Unmarshal([]byte(value), &event); err != nil {
        return nil, errors.New("unmarshal_event_failed")
    }

    ret := make(map[string]interface{})
    if err := json.Unmarshal([]byte(event.Extra), &ret); err != nil {
        return nil, errors.New("unmarshal_extra_failed")
    }

    return ret, nil
}

比如我們希望第一次unmarshal成功,第二次也成功师溅,我們可以撰寫如下單測(cè)

func TestParseEvent(t *testing.T) {
    PatchConvey("test parse event", t, func() {
        PatchConvey("test success", func() {
            Mock(json.Unmarshal).Return(nil).Build() // 一次mock后續(xù)所有執(zhí)行全部都是這個(gè)結(jié)果
            ret, err := ParseEvent("")
            So(ret, ShouldNotBeNil)
            So(err, ShouldBeNil)
        })
    })
}

但是如果我希望第一次成功茅信,第二次失敗呢,使用上述方式就行不通了墓臭,我們可以這樣寫

func TestParseEvent(t *testing.T) {
    PatchConvey("test parse event", t, func() {
        PatchConvey("test unmarshal extra failed", func() {
            Mock(json.Unmarshal).Return(Sequence(nil).Then(errors.New("unmarshal failed"))).Build()
            ret, err := ParseEvent("")
            So(ret, ShouldBeNil)
            So(err.Error(), ShouldEqual, "unmarshal_extra_failed")
        })
    })
}

你可能會(huì)問(wèn)蘸鲸,那我連續(xù)mock兩次json.unmarshal是否可以的,答案當(dāng)然是no窿锉,連續(xù)mock會(huì)導(dǎo)致異常酌摇,如:


func TestParseEvent(t *testing.T) {
    PatchConvey("test parse event", t, func() {
        PatchConvey("test unmarshal extra failed", func() {
            // Mock(json.Unmarshal).Return(Sequence(nil).Then(errors.New("unmarshal failed"))).Build()
            Mock(json.Unmarshal).Return(nil).Build()
            Mock(json.Unmarshal).Return(errors.New("unmarshal failed")).Build()
            ret, err := ParseEvent("")
            So(ret, ShouldBeNil)
            So(err.Error(), ShouldEqual, "unmarshal_extra_failed")
        })
    })
}

運(yùn)行結(jié)果如下:會(huì)提示re-mock
 Line 51: - re-mock <func([]uint8, interface {}) error Value>, previous mock at: /Users/bytedance/go/src/code.byted.org/namespace/test/unittest/exemple_test.go:50 
  goroutine 6 [running]:

FAQ

請(qǐng)移步參考官方文檔
!N嗽亍窑多!這里建議執(zhí)行單測(cè)是默認(rèn)關(guān)閉內(nèi)聯(lián)優(yōu)化,這樣可以保證mock成功洼滚。

總結(jié)

保持寫單測(cè)是一個(gè)很好的習(xí)慣埂息,它可以輔助我們驗(yàn)證程序的邏輯正確性,同時(shí)讓我們?cè)谥貥?gòu)一些代碼時(shí)更加有信心控制風(fēng)險(xiǎn)(代碼覆蓋率足夠高的前提下)同時(shí)也可以讓我們放心的將一些代碼交給新同學(xué)來(lái)開發(fā)。
雖然寫單測(cè)會(huì)耗費(fèi)一定的時(shí)間和精力千康,但總比線上出了問(wèn)題擦屁股復(fù)盤強(qiáng)享幽,不是嗎?
目前筆者也還是小白階段拾弃,會(huì)持續(xù)將實(shí)際工作中遇到的問(wèn)題總結(jié)到本文值桩,就算是當(dāng)做學(xué)習(xí)筆記吧。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末豪椿,一起剝皮案震驚了整個(gè)濱河市奔坟,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌砂碉,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件刻两,死亡現(xiàn)場(chǎng)離奇詭異增蹭,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)磅摹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門滋迈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人户誓,你說(shuō)我怎么就攤上這事饼灿。” “怎么了帝美?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵碍彭,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我悼潭,道長(zhǎng)庇忌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任舰褪,我火速辦了婚禮皆疹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘占拍。我一直安慰自己略就,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布晃酒。 她就那樣靜靜地躺著表牢,像睡著了一般。 火紅的嫁衣襯著肌膚如雪贝次。 梳的紋絲不亂的頭發(fā)上初茶,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼恼布。 笑死螺戳,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的折汞。 我是一名探鬼主播倔幼,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼爽待!你這毒婦竟也來(lái)了损同?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤鸟款,失蹤者是張志新(化名)和其女友劉穎膏燃,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體何什,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡组哩,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了处渣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片伶贰。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖罐栈,靈堂內(nèi)的尸體忽然破棺而出黍衙,到底是詐尸還是另有隱情,我是刑警寧澤荠诬,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布琅翻,位于F島的核電站,受9級(jí)特大地震影響柑贞,放射性物質(zhì)發(fā)生泄漏望迎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一凌外、第九天 我趴在偏房一處隱蔽的房頂上張望辩尊。 院中可真熱鬧,春花似錦康辑、人聲如沸摄欲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)胸墙。三九已至,卻和暖如春按咒,著一層夾襖步出監(jiān)牢的瞬間迟隅,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留智袭,地道東北人奔缠。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像吼野,于是被迫代替她去往敵國(guó)和親校哎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

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