欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Go語言單元測試基礎(chǔ)從入門到放棄

 更新時間:2022年06月21日 15:42:08   作者:李文周  
這篇文章主要介紹了Go單元測試基礎(chǔ)從入門到放棄為大家開啟Go語言單元測試第一篇章,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

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/asserttestify/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)的問題

    這篇文章主要介紹了解決golang post文件時Content-Type出現(xiàn)的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-05-05
  • Golang標(biāo)準(zhǔn)庫container/list的用法圖文詳解

    Golang標(biāo)準(zhǔn)庫container/list的用法圖文詳解

    提到單向鏈表,大家應(yīng)該是比較熟悉的了,這篇文章主要為大家詳細(xì)介紹了Golang標(biāo)準(zhǔn)庫container/list的用法相關(guān)知識,感興趣的小伙伴可以了解下
    2024-01-01
  • 詳解Golang中日志庫glog的使用

    詳解Golang中日志庫glog的使用

    golang/glog?是?C++?版本?google/glog?的?Go?版本實現(xiàn),基本實現(xiàn)了原生?glog?的日志格式,下面大家就跟隨小編一起了解一下glog的具體使用吧
    2023-09-09
  • 詳解Go語言中iota的應(yīng)用

    詳解Go語言中iota的應(yīng)用

    在本文中,小編將帶著大家深入探討?iota?的神奇力量,包括?iota?的介紹和應(yīng)用場景以及使用技巧和注意事項,準(zhǔn)備好了嗎,準(zhǔn)備一杯你最喜歡的飲料或茶,隨著本文一探究竟吧
    2023-07-07
  • Go語言包管理工具Godep的用法

    Go語言包管理工具Godep的用法

    這篇文章介紹了Go語言包管理工具Godep的用法,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-07-07
  • Golang常用包使用介紹

    Golang常用包使用介紹

    標(biāo)準(zhǔn)的Go語言代碼庫中包含了大量的包,并且在安裝Go的時候多數(shù)會自動安裝到系統(tǒng)中。我們可以在$GOROOT/src/pkg目錄中查看這些包。下面簡單介紹一些我們開發(fā)中常用的包
    2022-09-09
  • 詳解golang channel有無緩沖區(qū)的區(qū)別

    詳解golang channel有無緩沖區(qū)的區(qū)別

    這篇文章主要給大家介紹了golang channel有無緩沖區(qū)的區(qū)別,無緩沖是同步的,有緩沖是異步的,文中通過代碼示例給大家講解的非常詳細(xì),需要的朋友可以參考下
    2024-01-01
  • 使用Go HTTP客戶端打造高性能服務(wù)

    使用Go HTTP客戶端打造高性能服務(wù)

    大多數(shù)語言都有提供各自的 HTTP 客戶端,本文將動手實踐如何使用Go語言發(fā)起HTTP請求,并討論其中有可能遇到的問題。具有一定的參考價值,感興趣的可以了解一下
    2021-12-12
  • GoLand如何設(shè)置中文

    GoLand如何設(shè)置中文

    這篇文章主要介紹了GoLand如何設(shè)置中文,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-12-12
  • Go 代碼生成工具詳解

    Go 代碼生成工具詳解

    這篇文章主要介紹了Go 代碼生成工具詳解,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2021-03-03

最新評論