前言
越來(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í)筆記吧。