Go語(yǔ)言單元測(cè)試基礎(chǔ)從入門到放棄
Go語(yǔ)言測(cè)試
這是Go單測(cè)從入門到放棄系列教程的第0篇,主要講解在Go語(yǔ)言中如何做單元測(cè)試以及介紹了表格驅(qū)動(dòng)測(cè)試、回歸測(cè)試,并且介紹了常用的斷言工具。
go test工具
Go語(yǔ)言中的測(cè)試依賴go test命令。編寫測(cè)試代碼和編寫普通的Go代碼過程是類似的,并不需要學(xué)習(xí)新的語(yǔ)法、規(guī)則或工具。
go test命令是一個(gè)按照一定約定和組織的測(cè)試代碼的驅(qū)動(dòng)程序。在包目錄內(nèi),所有以_test.go為后綴名的源代碼文件都是go test測(cè)試的一部分,不會(huì)被go build編譯到最終的可執(zhí)行文件中。
在*_test.go文件中有三種類型的函數(shù),單元測(cè)試函數(shù)、基準(zhǔn)測(cè)試函數(shù)和示例函數(shù)。
| 類型 | 格式 | 作用 |
|---|---|---|
| 測(cè)試函數(shù) | 函數(shù)名前綴為Test | 測(cè)試程序的一些邏輯行為是否正確 |
| 基準(zhǔn)函數(shù) | 函數(shù)名前綴為Benchmark | 測(cè)試函數(shù)的性能 |
| 示例函數(shù) | 函數(shù)名前綴為Example | 為文檔提供示例文檔 |
go test命令會(huì)遍歷所有的*_test.go文件中符合上述命名規(guī)則的函數(shù),然后生成一個(gè)臨時(shí)的main包用于調(diào)用相應(yīng)的測(cè)試函數(shù),然后構(gòu)建并運(yùn)行、報(bào)告測(cè)試結(jié)果,最后清理測(cè)試中生成的臨時(shí)文件。
單元測(cè)試函數(shù)
格式
每個(gè)測(cè)試函數(shù)必須導(dǎo)入testing包,測(cè)試函數(shù)的基本格式(簽名)如下:
func?TestName(t?*testing.T){
????//?...
}
測(cè)試函數(shù)的名字必須以Test開頭,可選的后綴名必須以大寫字母開頭,舉幾個(gè)例子:
func?TestAdd(t?*testing.T){?...?}
func?TestSum(t?*testing.T){?...?}
func?TestLog(t?*testing.T){?...?}
其中參數(shù)t用于報(bào)告測(cè)試失敗和附加的日志信息。testing.T的擁有的方法如下:
func?(c?*T)?Cleanup(func())
func?(c?*T)?Error(args?...interface{})
func?(c?*T)?Errorf(format?string,?args?...interface{})
func?(c?*T)?Fail()
func?(c?*T)?FailNow()
func?(c?*T)?Failed()?bool
func?(c?*T)?Fatal(args?...interface{})
func?(c?*T)?Fatalf(format?string,?args?...interface{})
func?(c?*T)?Helper()
func?(c?*T)?Log(args?...interface{})
func?(c?*T)?Logf(format?string,?args?...interface{})
func?(c?*T)?Name()?string
func?(c?*T)?Skip(args?...interface{})
func?(c?*T)?SkipNow()
func?(c?*T)?Skipf(format?string,?args?...interface{})
func?(c?*T)?Skipped()?bool
func?(c?*T)?TempDir()?string
單元測(cè)試示例
就像細(xì)胞是構(gòu)成我們身體的基本單位,一個(gè)軟件程序也是由很多單元組件構(gòu)成的。單元組件可以是函數(shù)、結(jié)構(gòu)體、方法和最終用戶可能依賴的任意東西??傊覀冃枰_保這些組件是能夠正常運(yùn)行的。單元測(cè)試是一些利用各種方法測(cè)試單元組件的程序,它會(huì)將結(jié)果與預(yù)期輸出進(jìn)行比較。
接下來(lái),我們?cè)?code>base_demo包中定義了一個(gè)Split函數(shù),具體實(shí)現(xiàn)如下:
//?base_demo/split.go
package?base_demo
import?"strings"
//?Split?把字符串s按照給定的分隔符sep進(jìn)行分割返回字符串切片
func?Split(s,?sep?string)?(result?[]string)?{
?i?:=?strings.Index(s,?sep)
?for?i?>?-1?{
??result?=?append(result,?s[:i])
??s?=?s[i+1:]
??i?=?strings.Index(s,?sep)
?}
?result?=?append(result,?s)
?return
}
在當(dāng)前目錄下,我們創(chuàng)建一個(gè)split_test.go的測(cè)試文件,并定義一個(gè)測(cè)試函數(shù)如下:
//?split/split_test.go
package?split
import?(
?"reflect"
?"testing"
)
func?TestSplit(t?*testing.T)?{?//?測(cè)試函數(shù)名必須以Test開頭,必須接收一個(gè)*testing.T類型參數(shù)
?got?:=?Split("a:b:c",?":")?????????//?程序輸出的結(jié)果
?want?:=?[]string{"a",?"b",?"c"}????//?期望的結(jié)果
?if?!reflect.DeepEqual(want,?got)?{?//?因?yàn)閟lice不能比較直接,借助反射包中的方法比較
??t.Errorf("expected:%v,?got:%v",?want,?got)?//?測(cè)試失敗輸出錯(cuò)誤提示
?}
}
此時(shí)split這個(gè)包中的文件如下:
??ls?-l total?16 -rw-r--r--??1?liwenzhou??staff??408??4?29?15:50?split.go -rw-r--r--??1?liwenzhou??staff??466??4?29?16:04?split_test.go
在當(dāng)前路徑下執(zhí)行go test命令,可以看到輸出結(jié)果如下:
? go test
PASS
ok golang-unit-test-demo/base_demo 0.005s
go test -v
一個(gè)測(cè)試用例有點(diǎn)單薄,我們?cè)倬帉懸粋€(gè)測(cè)試使用多個(gè)字符切割字符串的例子,在split_test.go中添加如下測(cè)試函數(shù):
func?TestSplitWithComplexSep(t?*testing.T)?{
?got?:=?Split("abcd",?"bc")
?want?:=?[]string{"a",?"d"}
?if?!reflect.DeepEqual(want,?got)?{
??t.Errorf("expected:%v,?got:%v",?want,?got)
?}
}
現(xiàn)在我們有多個(gè)測(cè)試用例了,為了能更好的在輸出結(jié)果中看到每個(gè)測(cè)試用例的執(zhí)行情況,我們可以為go test命令添加-v參數(shù),讓它輸出完整的測(cè)試結(jié)果。
? go test -v
=== RUN TestSplit
--- PASS: TestSplit (0.00s)
=== RUN TestSplitWithComplexSep
split_test.go:20: expected:[a d], got:[a cd]
--- FAIL: TestSplitWithComplexSep (0.00s)
FAIL
exit status 1
FAIL golang-unit-test-demo/base_demo 0.009s
從上面的輸出結(jié)果我們能清楚的看到是TestSplitWithComplexSep這個(gè)測(cè)試用例沒有測(cè)試通過。
go test -run
單元測(cè)試的結(jié)果表明split函數(shù)的實(shí)現(xiàn)并不可靠,沒有考慮到傳入的sep參數(shù)是多個(gè)字符的情況,下面我們來(lái)修復(fù)下這個(gè)Bug:
package?base_demo
import?"strings"
//?Split?把字符串s按照給定的分隔符sep進(jìn)行分割返回字符串切片
func?Split(s,?sep?string)?(result?[]string)?{
?i?:=?strings.Index(s,?sep)
?for?i?>?-1?{
??result?=?append(result,?s[:i])
??s?=?s[i+len(sep):]?//?這里使用len(sep)獲取sep的長(zhǎng)度
??i?=?strings.Index(s,?sep)
?}
?result?=?append(result,?s)
?return
}
在執(zhí)行go test命令的時(shí)候可以添加-run參數(shù),它對(duì)應(yīng)一個(gè)正則表達(dá)式,只有函數(shù)名匹配上的測(cè)試函數(shù)才會(huì)被go test命令執(zhí)行。
例如通過給go test添加-run=Sep參數(shù)來(lái)告訴它本次測(cè)試只運(yùn)行TestSplitWithComplexSep這個(gè)測(cè)試用例:
??go?test?-run=Sep?-v ===?RUN???TestSplitWithComplexSep ---?PASS:?TestSplitWithComplexSep?(0.00s) PASS ok??????golang-unit-test-demo/base_demo?0.010s
最終的測(cè)試結(jié)果表情我們成功修復(fù)了之前的Bug。
回歸測(cè)試
我們修改了代碼之后僅僅執(zhí)行那些失敗的測(cè)試用例或新引入的測(cè)試用例是錯(cuò)誤且危險(xiǎn)的,正確的做法應(yīng)該是完整運(yùn)行所有的測(cè)試用例,保證不會(huì)因?yàn)樾薷拇a而引入新的問題。
??go?test?-v ===?RUN???TestSplit ---?PASS:?TestSplit?(0.00s) ===?RUN???TestSplitWithComplexSep ---?PASS:?TestSplitWithComplexSep?(0.00s) PASS ok??????golang-unit-test-demo/base_demo?0.011s
測(cè)試結(jié)果表明我們的單元測(cè)試全部通過。
通過這個(gè)示例我們可以看到,有了單元測(cè)試就能夠在代碼改動(dòng)后快速進(jìn)行回歸測(cè)試,極大地提高開發(fā)效率并保證代碼的質(zhì)量。
跳過某些測(cè)試用例
為了節(jié)省時(shí)間支持在單元測(cè)試時(shí)跳過某些耗時(shí)的測(cè)試用例。
func?TestTimeConsuming(t?*testing.T)?{
????if?testing.Short()?{
????????t.Skip("short模式下會(huì)跳過該測(cè)試用例")
????}
????...
}
當(dāng)執(zhí)行go test -short時(shí)就不會(huì)執(zhí)行上面的TestTimeConsuming測(cè)試用例。
子測(cè)試
在上面的示例中我們?yōu)槊恳粋€(gè)測(cè)試數(shù)據(jù)編寫了一個(gè)測(cè)試函數(shù),而通常單元測(cè)試中需要多組測(cè)試數(shù)據(jù)保證測(cè)試的效果。Go1.7+ 中新增了子測(cè)試,支持在測(cè)試函數(shù)中使用t.Run執(zhí)行一組測(cè)試用例,這樣就不需要為不同的測(cè)試數(shù)據(jù)定義多個(gè)測(cè)試函數(shù)了。
func?TestXXX(t?*testing.T){
??t.Run("case1",?func(t?*testing.T){...})
??t.Run("case2",?func(t?*testing.T){...})
??t.Run("case3",?func(t?*testing.T){...})
}
表格驅(qū)動(dòng)測(cè)試
介紹
編寫好的測(cè)試并非易事,但在許多情況下,表格驅(qū)動(dòng)測(cè)試可以涵蓋很多方面:表格里的每一個(gè)條目都是一個(gè)完整的測(cè)試用例,包含輸入和預(yù)期結(jié)果,有時(shí)還包含測(cè)試名稱等附加信息,以使測(cè)試輸出易于閱讀。
使用表格驅(qū)動(dòng)測(cè)試能夠很方便的維護(hù)多個(gè)測(cè)試用例,避免在編寫單元測(cè)試時(shí)頻繁的復(fù)制粘貼。
表格驅(qū)動(dòng)測(cè)試的步驟通常是定義一個(gè)測(cè)試用例表格,然后遍歷表格,并使用t.Run對(duì)每個(gè)條目執(zhí)行必要的測(cè)試。
表格驅(qū)動(dòng)測(cè)試不是工具、包或其他任何東西,它只是編寫更清晰測(cè)試的一種方式和視角。
示例
官方標(biāo)準(zhǔn)庫(kù)中有很多表格驅(qū)動(dòng)測(cè)試的示例,例如fmt包中的測(cè)試代碼:
var?flagtests?=?[]struct?{
?in??string
?out?string
}{
?{"%a",?"[%a]"},
?{"%-a",?"[%-a]"},
?{"%+a",?"[%+a]"},
?{"%#a",?"[%#a]"},
?{"%?a",?"[%?a]"},
?{"%0a",?"[%0a]"},
?{"%1.2a",?"[%1.2a]"},
?{"%-1.2a",?"[%-1.2a]"},
?{"%+1.2a",?"[%+1.2a]"},
?{"%-+1.2a",?"[%+-1.2a]"},
?{"%-+1.2abc",?"[%+-1.2a]bc"},
?{"%-1.2abc",?"[%-1.2a]bc"},
}
func?TestFlagParser(t?*testing.T)?{
?var?flagprinter?flagPrinter
?for?_,?tt?:=?range?flagtests?{
??t.Run(tt.in,?func(t?*testing.T)?{
???s?:=?Sprintf(tt.in,?&flagprinter)
???if?s?!=?tt.out?{
????t.Errorf("got?%q,?want?%q",?s,?tt.out)
???}
??})
?}
}
通常表格是匿名結(jié)構(gòu)體數(shù)組切片,可以定義結(jié)構(gòu)體或使用已經(jīng)存在的結(jié)構(gòu)進(jìn)行結(jié)構(gòu)體數(shù)組聲明。name屬性用來(lái)描述特定的測(cè)試用例。
接下來(lái)讓我們?cè)囍约壕帉懕砀耱?qū)動(dòng)測(cè)試:
func?TestSplitAll(t?*testing.T)?{
?//?定義測(cè)試表格
?//?這里使用匿名結(jié)構(gòu)體定義了若干個(gè)測(cè)試用例
?//?并且為每個(gè)測(cè)試用例設(shè)置了一個(gè)名稱
?tests?:=?[]struct?{
??name??string
??input?string
??sep???string
??want??[]string
?}{
??{"base?case",?"a:b:c",?":",?[]string{"a",?"b",?"c"}},
??{"wrong?sep",?"a:b:c",?",",?[]string{"a:b:c"}},
??{"more?sep",?"abcd",?"bc",?[]string{"a",?"d"}},
??{"leading?sep",?"沙河有沙又有河",?"沙",?[]string{"",?"河有",?"又有河"}},
?}
?//?遍歷測(cè)試用例
?for?_,?tt?:=?range?tests?{
??t.Run(tt.name,?func(t?*testing.T)?{?//?使用t.Run()執(zhí)行子測(cè)試
???got?:=?Split(tt.input,?tt.sep)
???if?!reflect.DeepEqual(got,?tt.want)?{
????t.Errorf("expected:%#v,?got:%#v",?tt.want,?got)
???}
??})
?}
}
在終端執(zhí)行go test -v,會(huì)得到如下測(cè)試輸出結(jié)果:
? go test -v
=== RUN TestSplit
--- PASS: TestSplit (0.00s)
=== RUN TestSplitWithComplexSep
--- PASS: TestSplitWithComplexSep (0.00s)
=== RUN TestSplitAll
=== RUN TestSplitAll/base_case
=== RUN TestSplitAll/wrong_sep
=== RUN TestSplitAll/more_sep
=== RUN TestSplitAll/leading_sep
--- PASS: TestSplitAll (0.00s)
--- PASS: TestSplitAll/base_case (0.00s)
--- PASS: TestSplitAll/wrong_sep (0.00s)
--- PASS: TestSplitAll/more_sep (0.00s)
--- PASS: TestSplitAll/leading_sep (0.00s)
PASS
ok golang-unit-test-demo/base_demo 0.010s
并行測(cè)試
表格驅(qū)動(dòng)測(cè)試中通常會(huì)定義比較多的測(cè)試case,在Go語(yǔ)言中很容易發(fā)揮自身并發(fā)優(yōu)勢(shì)將表格驅(qū)動(dòng)測(cè)試并行化,可以查看下面的代碼示例。
func?TestSplitAll(t?*testing.T)?{
?t.Parallel()??//?將?TLog?標(biāo)記為能夠與其他測(cè)試并行運(yùn)行
?//?定義測(cè)試表格
?//?這里使用匿名結(jié)構(gòu)體定義了若干個(gè)測(cè)試用例
?//?并且為每個(gè)測(cè)試用例設(shè)置了一個(gè)名稱
?tests?:=?[]struct?{
??name??string
??input?string
??sep???string
??want??[]string
?}{
??{"base?case",?"a:b:c",?":",?[]string{"a",?"b",?"c"}},
??{"wrong?sep",?"a:b:c",?",",?[]string{"a:b:c"}},
??{"more?sep",?"abcd",?"bc",?[]string{"a",?"d"}},
??{"leading?sep",?"沙河有沙又有河",?"沙",?[]string{"",?"河有",?"又有河"}},
?}
?//?遍歷測(cè)試用例
?for?_,?tt?:=?range?tests?{
??tt?:=?tt??//?注意這里重新聲明tt變量(避免多個(gè)goroutine中使用了相同的變量)
??t.Run(tt.name,?func(t?*testing.T)?{?//?使用t.Run()執(zhí)行子測(cè)試
???t.Parallel()??//?將每個(gè)測(cè)試用例標(biāo)記為能夠彼此并行運(yùn)行
???got?:=?Split(tt.input,?tt.sep)
???if?!reflect.DeepEqual(got,?tt.want)?{
????t.Errorf("expected:%#v,?got:%#v",?tt.want,?got)
???}
??})
?}
}
使用工具生成測(cè)試代碼
社區(qū)里有很多自動(dòng)生成表格驅(qū)動(dòng)測(cè)試函數(shù)的工具,比如gotests等,很多編輯器如Goland也支持快速生成測(cè)試文件。這里簡(jiǎn)單演示一下gotests的使用。
安裝
go?get?-u?github.com/cweill/gotests/...
執(zhí)行
gotests?-all?-w?split.go
上面的命令表示,為split.go文件的所有函數(shù)生成測(cè)試代碼至split_test.go文件(目錄下如果事先存在這個(gè)文件就不再生成)。
生成的測(cè)試代碼大致如下:
package?base_demo
import?(
?"reflect"
?"testing"
)
func?TestSplit(t?*testing.T)?{
?type?args?struct?{
??s???string
??sep?string
?}
?tests?:=?[]struct?{
??name???????string
??args???????args
??wantResult?[]string
?}{
??//?TODO:?Add?test?cases.
?}
?for?_,?tt?:=?range?tests?{
??t.Run(tt.name,?func(t?*testing.T)?{
???if?gotResult?:=?Split(tt.args.s,?tt.args.sep);?!reflect.DeepEqual(gotResult,?tt.wantResult)?{
????t.Errorf("Split()?=?%v,?want?%v",?gotResult,?tt.wantResult)
???}
??})
?}
}
代碼格式與我們上面的類似,只需要在TODO位置添加我們的測(cè)試邏輯就可以了。
測(cè)試覆蓋率
測(cè)試覆蓋率是指代碼被測(cè)試套件覆蓋的百分比。通常我們使用的都是語(yǔ)句的覆蓋率,也就是在測(cè)試中至少被運(yùn)行一次的代碼占總代碼的比例。在公司內(nèi)部一般會(huì)要求測(cè)試覆蓋率達(dá)到80%左右。
Go提供內(nèi)置功能來(lái)檢查你的代碼覆蓋率。我們可以使用go test -cover來(lái)查看測(cè)試覆蓋率。例如:
??go?test?-cover PASS coverage:?100.0%?of?statements ok??????golang-unit-test-demo/base_demo?0.009s
從上面的結(jié)果可以看到我們的測(cè)試用例覆蓋了100%的代碼。
Go還提供了一個(gè)額外的-coverprofile參數(shù),用來(lái)將覆蓋率相關(guān)的記錄信息輸出到一個(gè)文件。例如:
??go?test?-cover?-coverprofile=c.out PASS coverage:?100.0%?of?statements ok??????golang-unit-test-demo/base_demo?0.009s
上面的命令會(huì)將覆蓋率相關(guān)的信息輸出到當(dāng)前文件夾下面的c.out文件中。
??tree?. . ├──?c.out ├──?split.go └──?split_test.go
然后我們執(zhí)行go tool cover -html=c.out,使用cover工具來(lái)處理生成的記錄信息,該命令會(huì)打開本地的瀏覽器窗口生成一個(gè)HTML報(bào)告。

上圖中每個(gè)用綠色標(biāo)記的語(yǔ)句塊表示被覆蓋了,而紅色的表示沒有被覆蓋。
testify/assert
testify是一個(gè)社區(qū)非常流行的Go單元測(cè)試工具包,其中使用最多的功能就是它提供的斷言工具——testify/assert或testify/require。
安裝
go?get?github.com/stretchr/testify
使用示例
我們?cè)趯憜卧獪y(cè)試的時(shí)候,通常需要使用斷言來(lái)校驗(yàn)測(cè)試結(jié)果,但是由于Go語(yǔ)言中沒有提供斷言,所以我們會(huì)寫出很多的if...else...語(yǔ)句。而testify/assert為我們提供了很多常用的斷言函數(shù),并且能夠輸出友好、易于閱讀的錯(cuò)誤描述信息。
比如我們之前在TestSplit測(cè)試函數(shù)中就使用了reflect.DeepEqual來(lái)判斷期望結(jié)果與實(shí)際結(jié)果是否一致。
t.Run(tt.name,?func(t?*testing.T)?{?//?使用t.Run()執(zhí)行子測(cè)試
?got?:=?Split(tt.input,?tt.sep)
?if?!reflect.DeepEqual(got,?tt.want)?{
??t.Errorf("expected:%#v,?got:%#v",?tt.want,?got)
?}
})
使用testify/assert之后就能將上述判斷過程簡(jiǎn)化如下:
t.Run(tt.name,?func(t?*testing.T)?{?//?使用t.Run()執(zhí)行子測(cè)試
?got?:=?Split(tt.input,?tt.sep)
?assert.Equal(t,?got,?tt.want)??//?使用assert提供的斷言函數(shù)
})
當(dāng)我們有多個(gè)斷言語(yǔ)句時(shí),還可以使用assert := assert.New(t)創(chuàng)建一個(gè)assert對(duì)象,它擁有前面所有的斷言方法,只是不需要再傳入Testing.T參數(shù)了。
func?TestSomething(t?*testing.T)?{
??assert?:=?assert.New(t)
??//?assert?equality
??assert.Equal(123,?123,?"they?should?be?equal")
??//?assert?inequality
??assert.NotEqual(123,?456,?"they?should?not?be?equal")
??//?assert?for?nil?(good?for?errors)
??assert.Nil(object)
??//?assert?for?not?nil?(good?when?you?expect?something)
??if?assert.NotNil(object)?{
????//?now?we?know?that?object?isn't?nil,?we?are?safe?to?make
????//?further?assertions?without?causing?any?errors
????assert.Equal("Something",?object.Value)
??}
}
testify/assert提供了非常多的斷言函數(shù),這里沒辦法一一列舉出來(lái),大家可以查看官方文檔了解。
testify/require擁有testify/assert所有斷言函數(shù),它們的唯一區(qū)別就是——testify/require遇到失敗的用例會(huì)立即終止本次測(cè)試。
此外,testify包還提供了mock、http等其他測(cè)試工具,篇幅所限這里就不詳細(xì)介紹了,有興趣的同學(xué)可以自己了解一下。
總結(jié)
本文介紹了Go語(yǔ)言單元測(cè)試的基本用法,通過為Split函數(shù)編寫單元測(cè)試的真實(shí)案例,模擬了日常開發(fā)過程中的場(chǎng)景,一步一步詳細(xì)介紹了表格驅(qū)動(dòng)測(cè)試、回歸測(cè)試和常用的斷言工具testify/assert的使用。在下一篇中,我們將更進(jìn)一步,詳細(xì)介紹如何使用httptest和gock工具進(jìn)行網(wǎng)絡(luò)測(cè)試,更多關(guān)于Go語(yǔ)言單元測(cè)試基礎(chǔ)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
解決golang post文件時(shí)Content-Type出現(xiàn)的問題
這篇文章主要介紹了解決golang post文件時(shí)Content-Type出現(xiàn)的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來(lái)看看吧2021-05-05
Golang標(biāo)準(zhǔn)庫(kù)container/list的用法圖文詳解
提到單向鏈表,大家應(yīng)該是比較熟悉的了,這篇文章主要為大家詳細(xì)介紹了Golang標(biāo)準(zhǔn)庫(kù)container/list的用法相關(guān)知識(shí),感興趣的小伙伴可以了解下2024-01-01
詳解golang channel有無(wú)緩沖區(qū)的區(qū)別
這篇文章主要給大家介紹了golang channel有無(wú)緩沖區(qū)的區(qū)別,無(wú)緩沖是同步的,有緩沖是異步的,文中通過代碼示例給大家講解的非常詳細(xì),需要的朋友可以參考下2024-01-01

