Go語言單元測試基礎(chǔ)從入門到放棄
Go語言測試
這是Go單測從入門到放棄系列教程的第0篇,主要講解在Go語言中如何做單元測試以及介紹了表格驅(qū)動測試、回歸測試,并且介紹了常用的斷言工具。
go test工具
Go語言中的測試依賴go test
命令。編寫測試代碼和編寫普通的Go代碼過程是類似的,并不需要學(xué)習(xí)新的語法、規(guī)則或工具。
go test命令是一個按照一定約定和組織的測試代碼的驅(qū)動程序。在包目錄內(nèi),所有以_test.go
為后綴名的源代碼文件都是go test
測試的一部分,不會被go build
編譯到最終的可執(zhí)行文件中。
在*_test.go
文件中有三種類型的函數(shù),單元測試函數(shù)、基準(zhǔn)測試函數(shù)和示例函數(shù)。
類型 | 格式 | 作用 |
---|---|---|
測試函數(shù) | 函數(shù)名前綴為Test | 測試程序的一些邏輯行為是否正確 |
基準(zhǔn)函數(shù) | 函數(shù)名前綴為Benchmark | 測試函數(shù)的性能 |
示例函數(shù) | 函數(shù)名前綴為Example | 為文檔提供示例文檔 |
go test
命令會遍歷所有的*_test.go
文件中符合上述命名規(guī)則的函數(shù),然后生成一個臨時的main包用于調(diào)用相應(yīng)的測試函數(shù),然后構(gòu)建并運行、報告測試結(jié)果,最后清理測試中生成的臨時文件。
單元測試函數(shù)
格式
每個測試函數(shù)必須導(dǎo)入testing
包,測試函數(shù)的基本格式(簽名)如下:
func?TestName(t?*testing.T){ ????//?... }
測試函數(shù)的名字必須以Test
開頭,可選的后綴名必須以大寫字母開頭,舉幾個例子:
func?TestAdd(t?*testing.T){?...?} func?TestSum(t?*testing.T){?...?} func?TestLog(t?*testing.T){?...?}
其中參數(shù)t
用于報告測試失敗和附加的日志信息。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
單元測試示例
就像細(xì)胞是構(gòu)成我們身體的基本單位,一個軟件程序也是由很多單元組件構(gòu)成的。單元組件可以是函數(shù)、結(jié)構(gòu)體、方法和最終用戶可能依賴的任意東西。總之我們需要確保這些組件是能夠正常運行的。單元測試是一些利用各種方法測試單元組件的程序,它會將結(jié)果與預(yù)期輸出進行比較。
接下來,我們在base_demo
包中定義了一個Split
函數(shù),具體實現(xiàn)如下:
//?base_demo/split.go package?base_demo import?"strings" //?Split?把字符串s按照給定的分隔符sep進行分割返回字符串切片 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)建一個split_test.go
的測試文件,并定義一個測試函數(shù)如下:
//?split/split_test.go package?split import?( ?"reflect" ?"testing" ) func?TestSplit(t?*testing.T)?{?//?測試函數(shù)名必須以Test開頭,必須接收一個*testing.T類型參數(shù) ?got?:=?Split("a:b:c",?":")?????????//?程序輸出的結(jié)果 ?want?:=?[]string{"a",?"b",?"c"}????//?期望的結(jié)果 ?if?!reflect.DeepEqual(want,?got)?{?//?因為slice不能比較直接,借助反射包中的方法比較 ??t.Errorf("expected:%v,?got:%v",?want,?got)?//?測試失敗輸出錯誤提示 ?} }
此時split
這個包中的文件如下:
??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
一個測試用例有點單薄,我們再編寫一個測試使用多個字符切割字符串的例子,在split_test.go
中添加如下測試函數(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)在我們有多個測試用例了,為了能更好的在輸出結(jié)果中看到每個測試用例的執(zhí)行情況,我們可以為go test
命令添加-v
參數(shù),讓它輸出完整的測試結(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
這個測試用例沒有測試通過。
go test -run
單元測試的結(jié)果表明split
函數(shù)的實現(xiàn)并不可靠,沒有考慮到傳入的sep參數(shù)是多個字符的情況,下面我們來修復(fù)下這個Bug:
package?base_demo import?"strings" //?Split?把字符串s按照給定的分隔符sep進行分割返回字符串切片 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的長度 ??i?=?strings.Index(s,?sep) ?} ?result?=?append(result,?s) ?return }
在執(zhí)行go test
命令的時候可以添加-run
參數(shù),它對應(yīng)一個正則表達(dá)式,只有函數(shù)名匹配上的測試函數(shù)才會被go test
命令執(zhí)行。
例如通過給go test
添加-run=Sep
參數(shù)來告訴它本次測試只運行TestSplitWithComplexSep
這個測試用例:
??go?test?-run=Sep?-v ===?RUN???TestSplitWithComplexSep ---?PASS:?TestSplitWithComplexSep?(0.00s) PASS ok??????golang-unit-test-demo/base_demo?0.010s
最終的測試結(jié)果表情我們成功修復(fù)了之前的Bug。
回歸測試
我們修改了代碼之后僅僅執(zhí)行那些失敗的測試用例或新引入的測試用例是錯誤且危險的,正確的做法應(yīng)該是完整運行所有的測試用例,保證不會因為修改代碼而引入新的問題。
??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
測試結(jié)果表明我們的單元測試全部通過。
通過這個示例我們可以看到,有了單元測試就能夠在代碼改動后快速進行回歸測試,極大地提高開發(fā)效率并保證代碼的質(zhì)量。
跳過某些測試用例
為了節(jié)省時間支持在單元測試時跳過某些耗時的測試用例。
func?TestTimeConsuming(t?*testing.T)?{ ????if?testing.Short()?{ ????????t.Skip("short模式下會跳過該測試用例") ????} ????... }
當(dāng)執(zhí)行go test -short
時就不會執(zhí)行上面的TestTimeConsuming
測試用例。
子測試
在上面的示例中我們?yōu)槊恳粋€測試數(shù)據(jù)編寫了一個測試函數(shù),而通常單元測試中需要多組測試數(shù)據(jù)保證測試的效果。Go1.7+ 中新增了子測試,支持在測試函數(shù)中使用t.Run
執(zhí)行一組測試用例,這樣就不需要為不同的測試數(shù)據(jù)定義多個測試函數(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ū)動測試
介紹
編寫好的測試并非易事,但在許多情況下,表格驅(qū)動測試可以涵蓋很多方面:表格里的每一個條目都是一個完整的測試用例,包含輸入和預(yù)期結(jié)果,有時還包含測試名稱等附加信息,以使測試輸出易于閱讀。
使用表格驅(qū)動測試能夠很方便的維護多個測試用例,避免在編寫單元測試時頻繁的復(fù)制粘貼。
表格驅(qū)動測試的步驟通常是定義一個測試用例表格,然后遍歷表格,并使用t.Run
對每個條目執(zhí)行必要的測試。
表格驅(qū)動測試不是工具、包或其他任何東西,它只是編寫更清晰測試的一種方式和視角。
示例
官方標(biāo)準(zhǔn)庫中有很多表格驅(qū)動測試的示例,例如fmt包中的測試代碼:
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)進行結(jié)構(gòu)體數(shù)組聲明。name屬性用來描述特定的測試用例。
接下來讓我們試著自己編寫表格驅(qū)動測試:
func?TestSplitAll(t?*testing.T)?{ ?//?定義測試表格 ?//?這里使用匿名結(jié)構(gòu)體定義了若干個測試用例 ?//?并且為每個測試用例設(shè)置了一個名稱 ?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{"",?"河有",?"又有河"}}, ?} ?//?遍歷測試用例 ?for?_,?tt?:=?range?tests?{ ??t.Run(tt.name,?func(t?*testing.T)?{?//?使用t.Run()執(zhí)行子測試 ???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
,會得到如下測試輸出結(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
并行測試
表格驅(qū)動測試中通常會定義比較多的測試case,在Go語言中很容易發(fā)揮自身并發(fā)優(yōu)勢將表格驅(qū)動測試并行化,可以查看下面的代碼示例。
func?TestSplitAll(t?*testing.T)?{ ?t.Parallel()??//?將?TLog?標(biāo)記為能夠與其他測試并行運行 ?//?定義測試表格 ?//?這里使用匿名結(jié)構(gòu)體定義了若干個測試用例 ?//?并且為每個測試用例設(shè)置了一個名稱 ?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{"",?"河有",?"又有河"}}, ?} ?//?遍歷測試用例 ?for?_,?tt?:=?range?tests?{ ??tt?:=?tt??//?注意這里重新聲明tt變量(避免多個goroutine中使用了相同的變量) ??t.Run(tt.name,?func(t?*testing.T)?{?//?使用t.Run()執(zhí)行子測試 ???t.Parallel()??//?將每個測試用例標(biāo)記為能夠彼此并行運行 ???got?:=?Split(tt.input,?tt.sep) ???if?!reflect.DeepEqual(got,?tt.want)?{ ????t.Errorf("expected:%#v,?got:%#v",?tt.want,?got) ???} ??}) ?} }
使用工具生成測試代碼
社區(qū)里有很多自動生成表格驅(qū)動測試函數(shù)的工具,比如gotests等,很多編輯器如Goland也支持快速生成測試文件。這里簡單演示一下gotests
的使用。
安裝
go?get?-u?github.com/cweill/gotests/...
執(zhí)行
gotests?-all?-w?split.go
上面的命令表示,為split.go
文件的所有函數(shù)生成測試代碼至split_test.go
文件(目錄下如果事先存在這個文件就不再生成)。
生成的測試代碼大致如下:
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位置添加我們的測試邏輯就可以了。
測試覆蓋率
測試覆蓋率是指代碼被測試套件覆蓋的百分比。通常我們使用的都是語句的覆蓋率,也就是在測試中至少被運行一次的代碼占總代碼的比例。在公司內(nèi)部一般會要求測試覆蓋率達(dá)到80%左右。
Go提供內(nèi)置功能來檢查你的代碼覆蓋率。我們可以使用go test -cover
來查看測試覆蓋率。例如:
??go?test?-cover PASS coverage:?100.0%?of?statements ok??????golang-unit-test-demo/base_demo?0.009s
從上面的結(jié)果可以看到我們的測試用例覆蓋了100%的代碼。
Go還提供了一個額外的-coverprofile
參數(shù),用來將覆蓋率相關(guān)的記錄信息輸出到一個文件。例如:
??go?test?-cover?-coverprofile=c.out PASS coverage:?100.0%?of?statements ok??????golang-unit-test-demo/base_demo?0.009s
上面的命令會將覆蓋率相關(guān)的信息輸出到當(dāng)前文件夾下面的c.out
文件中。
??tree?. . ├──?c.out ├──?split.go └──?split_test.go
然后我們執(zhí)行go tool cover -html=c.out
,使用cover
工具來處理生成的記錄信息,該命令會打開本地的瀏覽器窗口生成一個HTML報告。
上圖中每個用綠色標(biāo)記的語句塊表示被覆蓋了,而紅色的表示沒有被覆蓋。
testify/assert
testify是一個社區(qū)非常流行的Go單元測試工具包,其中使用最多的功能就是它提供的斷言工具——testify/assert
或testify/require
。
安裝
go?get?github.com/stretchr/testify
使用示例
我們在寫單元測試的時候,通常需要使用斷言來校驗測試結(jié)果,但是由于Go語言中沒有提供斷言,所以我們會寫出很多的if...else...
語句。而testify/assert
為我們提供了很多常用的斷言函數(shù),并且能夠輸出友好、易于閱讀的錯誤描述信息。
比如我們之前在TestSplit
測試函數(shù)中就使用了reflect.DeepEqual
來判斷期望結(jié)果與實際結(jié)果是否一致。
t.Run(tt.name,?func(t?*testing.T)?{?//?使用t.Run()執(zhí)行子測試 ?got?:=?Split(tt.input,?tt.sep) ?if?!reflect.DeepEqual(got,?tt.want)?{ ??t.Errorf("expected:%#v,?got:%#v",?tt.want,?got) ?} })
使用testify/assert
之后就能將上述判斷過程簡化如下:
t.Run(tt.name,?func(t?*testing.T)?{?//?使用t.Run()執(zhí)行子測試 ?got?:=?Split(tt.input,?tt.sep) ?assert.Equal(t,?got,?tt.want)??//?使用assert提供的斷言函數(shù) })
當(dāng)我們有多個斷言語句時,還可以使用assert := assert.New(t)創(chuàng)建一個assert對象,它擁有前面所有的斷言方法,只是不需要再傳入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ù),這里沒辦法一一列舉出來,大家可以查看官方文檔了解。
testify/require
擁有testify/assert
所有斷言函數(shù),它們的唯一區(qū)別就是——testify/require
遇到失敗的用例會立即終止本次測試。
此外,testify
包還提供了mock、http等其他測試工具,篇幅所限這里就不詳細(xì)介紹了,有興趣的同學(xué)可以自己了解一下。
總結(jié)
本文介紹了Go語言單元測試的基本用法,通過為Split函數(shù)編寫單元測試的真實案例,模擬了日常開發(fā)過程中的場景,一步一步詳細(xì)介紹了表格驅(qū)動測試、回歸測試和常用的斷言工具testify/assert的使用。在下一篇中,我們將更進一步,詳細(xì)介紹如何使用httptest和gock工具進行網(wǎng)絡(luò)測試,更多關(guān)于Go語言單元測試基礎(chǔ)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
解決golang post文件時Content-Type出現(xiàn)的問題
這篇文章主要介紹了解決golang post文件時Content-Type出現(xiàn)的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-05-05Golang標(biāo)準(zhǔn)庫container/list的用法圖文詳解
提到單向鏈表,大家應(yīng)該是比較熟悉的了,這篇文章主要為大家詳細(xì)介紹了Golang標(biāo)準(zhǔn)庫container/list的用法相關(guān)知識,感興趣的小伙伴可以了解下2024-01-01詳解golang channel有無緩沖區(qū)的區(qū)別
這篇文章主要給大家介紹了golang channel有無緩沖區(qū)的區(qū)別,無緩沖是同步的,有緩沖是異步的,文中通過代碼示例給大家講解的非常詳細(xì),需要的朋友可以參考下2024-01-01