序言
在軟件開發(fā)中浅乔,產(chǎn)品代碼的正確性通過測(cè)試代碼來保證靖苇,而測(cè)試代碼的正確性誰來保證贤壁?答案是毫無爭(zhēng)議的芯砸,肯定是程序員自己。這就要求測(cè)試代碼必須足夠簡(jiǎn)單且表達(dá)力強(qiáng)双揪,讓錯(cuò)誤無處藏身渔期。我們要有一個(gè)好鼻子疯趟,能夠嗅出測(cè)試的壞味道信峻,及時(shí)的進(jìn)行測(cè)試重構(gòu)盹舞,從而讓測(cè)試代碼易于維護(hù)踢步。筆者從大量的編碼實(shí)踐中感悟道:雖然能寫出好的產(chǎn)品代碼的程序員很牛获印,但能寫出好的測(cè)試代碼的程序員更牛街州,尤其對(duì)于TDD實(shí)踐。
要寫出好的測(cè)試代碼取募,必須精通相關(guān)的框架玩敏。對(duì)于Golang程序員來說旺聚,至少需要掌握下面兩個(gè)框架:
本文將主要介紹GoConvey框架的基本使用方法砰粹,從而指導(dǎo)讀者更好的進(jìn)行測(cè)試實(shí)踐碱璃,最終寫出簡(jiǎn)單優(yōu)雅的測(cè)試代碼嵌器。
GoConvey簡(jiǎn)介
GoConvey是一款針對(duì)Golang的測(cè)試框架爽航,可以管理和運(yùn)行測(cè)試用例讥珍,同時(shí)提供了豐富的斷言函數(shù)衷佃,并支持很多 Web 界面特性纲酗。
Golang雖然自帶了單元測(cè)試功能,并且在GoConvey框架誕生之前也出現(xiàn)了許多第三方測(cè)試框架右蕊,但沒有一個(gè)測(cè)試框架像GoConvey一樣能夠讓程序員如此簡(jiǎn)潔優(yōu)雅的編寫測(cè)試代碼帕翻。
安裝
在命令行運(yùn)行下面的命令:
go get github.com/smartystreets/goconvey
運(yùn)行時(shí)間較長(zhǎng),運(yùn)行完后你會(huì)發(fā)現(xiàn):
- 在$GOPATH/src目錄下新增了github.com子目錄紫岩,該子目錄里包含了GoConvey框架的庫代碼
- 在$GOPATH/bin目錄下新增了GoConvey框架的可執(zhí)行程序goconvey
注:上面是在gopath時(shí)代使用GoConvey的API前的安裝方法泉蝌,而在gomod時(shí)代一般不需要先顯式安裝(gomod機(jī)制會(huì)自動(dòng)從goproxy拉取依賴到本地cache)勋陪,除非要使用GoConvey的web界面诅愚,這時(shí)需要提前安裝GoConvey的二進(jìn)制劫映,命令為go install github.com/smartystreets/goconvey@latest等浊。
基本使用方法
我們通過一個(gè)案例來介紹GoConvey框架的基本使用方法筹燕,并對(duì)要點(diǎn)進(jìn)行歸納撒踪。
產(chǎn)品代碼
我們實(shí)現(xiàn)一個(gè)判斷兩個(gè)字符串切片是否相等的函數(shù)StringSliceEqual制妄,主要邏輯包括:
- 兩個(gè)字符串切片長(zhǎng)度不相等時(shí)耕捞,返回false
- 兩個(gè)字符串切片一個(gè)是nil俺抽,另一個(gè)不是nil時(shí)磷斧,返回false
- 遍歷兩個(gè)切片冕末,比較對(duì)應(yīng)索引的兩個(gè)切片元素值档桃,如果不相等藻肄,返回false
- 否則仅炊,返回true
根據(jù)上面的邏輯抚垄,代碼實(shí)現(xiàn)如下所示:
func StringSliceEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
if (a == nil) != (b == nil) {
return false
}
for i, v := range a {
if v != b[i] {
return false
}
}
return true
}
對(duì)于邏輯“兩個(gè)字符串切片一個(gè)是nil,另一個(gè)不是nil時(shí)毁兆,返回false”的實(shí)現(xiàn)代碼有點(diǎn)不好理解:
if (a == nil) != (b == nil) {
return false
}
我們實(shí)例化一下a和b气堕,即[]string{}和[]string(nil)茎芭,這時(shí)兩個(gè)字符串切片的長(zhǎng)度都是0梅桩,但肯定不相等宿百。
測(cè)試代碼
先寫一個(gè)正常情況的測(cè)試用例,如下所示:
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestStringSliceEqual(t *testing.T) {
Convey("TestStringSliceEqual should return true when a != nil && b != nil", t, func() {
a := []string{"hello", "goconvey"}
b := []string{"hello", "goconvey"}
So(StringSliceEqual(a, b), ShouldBeTrue)
})
}
由于GoConvey框架兼容Golang原生的單元測(cè)試雀费,所以可以使用go test -v來運(yùn)行測(cè)試。
打開命令行外臂,進(jìn)入$GOPATH/src/infra/alg目錄下坐儿,運(yùn)行g(shù)o test -v律胀,則測(cè)試用例的執(zhí)行結(jié)果日下:
=== RUN TestStringSliceEqual
TestStringSliceEqual should return true when a != nil && b != nil ?
1 total assertion
--- PASS: TestStringSliceEqual (0.00s)
PASS
ok infra/alg 0.006s
上面的測(cè)試用例代碼有如下幾個(gè)要點(diǎn):
- import goconvey包時(shí)宋光,前面加點(diǎn)號(hào)"."貌矿,以減少冗余的代碼。凡是在測(cè)試代碼中看到Convey和So兩個(gè)方法罪佳,肯定是convey包的精绎,不要在產(chǎn)品代碼中定義相同的函數(shù)名
- 測(cè)試函數(shù)的名字必須以Test開頭,而且參數(shù)類型必須為*testing.T
- 每個(gè)測(cè)試用例必須使用Convey函數(shù)包裹起來,它的第一個(gè)參數(shù)為string類型的測(cè)試描述,第二個(gè)參數(shù)為測(cè)試函數(shù)的入?yún)ⅲ愋蜑?testing.T),第三個(gè)參數(shù)為不接收任何參數(shù)也不返回任何值的函數(shù)(習(xí)慣使用閉包)
- Convey函數(shù)的第三個(gè)參數(shù)閉包的實(shí)現(xiàn)中通過So函數(shù)完成斷言判斷,它的第一個(gè)參數(shù)為實(shí)際值旗芬,第二個(gè)參數(shù)為斷言函數(shù)變量誊薄,第三個(gè)參數(shù)或者沒有(當(dāng)?shù)诙€(gè)參數(shù)為類ShouldBeTrue形式的函數(shù)變量)或者有(當(dāng)?shù)诙€(gè)函數(shù)為類ShouldEqual形式的函數(shù)變量)
我們故意將該測(cè)試用例改為不過:
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestStringSliceEqual(t *testing.T) {
Convey("TestStringSliceEqual should return true when a != nil && b != nil", t, func() {
a := []string{"hello", "goconvey"}
b := []string{"hello", "goconvey"}
So(StringSliceEqual(a, b), ShouldBeFalse)
})
}
測(cè)試用例的執(zhí)行結(jié)果日下:
=== RUN TestStringSliceEqual
TestStringSliceEqual should return true when a != nil && b != nil ?
Failures:
* /Users/zhangxiaolong/Desktop/D/go-workspace/src/infra/alg/slice_test.go
Line 45:
Expected: false
Actual: true
1 total assertion
--- FAIL: TestStringSliceEqual (0.00s)
FAIL
exit status 1
FAIL infra/alg 0.006s
我們?cè)傺a(bǔ)充3個(gè)測(cè)試用例:
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestStringSliceEqual(t *testing.T) {
Convey("TestStringSliceEqual should return true when a != nil && b != nil", t, func() {
a := []string{"hello", "goconvey"}
b := []string{"hello", "goconvey"}
So(StringSliceEqual(a, b), ShouldBeTrue)
})
Convey("TestStringSliceEqual should return true when a == nil && b == nil", t, func() {
So(StringSliceEqual(nil, nil), ShouldBeTrue)
})
Convey("TestStringSliceEqual should return false when a == nil && b != nil", t, func() {
a := []string(nil)
b := []string{}
So(StringSliceEqual(a, b), ShouldBeFalse)
})
Convey("TestStringSliceEqual should return false when a != nil && b != nil", t, func() {
a := []string{"hello", "world"}
b := []string{"hello", "goconvey"}
So(StringSliceEqual(a, b), ShouldBeFalse)
})
}
從上面的測(cè)試代碼可以看出定鸟,每一個(gè)Convey語句對(duì)應(yīng)一個(gè)測(cè)試用例,那么一個(gè)函數(shù)的多個(gè)測(cè)試用例可以通過一個(gè)測(cè)試函數(shù)的多個(gè)Convey語句來呈現(xiàn)。
測(cè)試用例的執(zhí)行結(jié)果如下:
=== RUN TestStringSliceEqual
TestStringSliceEqual should return true when a != nil && b != nil ?
1 total assertion
TestStringSliceEqual should return true when a == nil && b == nil ?
2 total assertions
TestStringSliceEqual should return false when a == nil && b != nil ?
3 total assertions
TestStringSliceEqual should return false when a != nil && b != nil ?
4 total assertions
--- PASS: TestStringSliceEqual (0.00s)
PASS
ok infra/alg 0.006s
Convey語句的嵌套
Convey語句可以無限嵌套挺峡,以體現(xiàn)測(cè)試用例之間的關(guān)系箫津。需要注意的是,只有最外層的Convey需要傳入*testing.T類型的變量t。
我們將前面的測(cè)試用例通過嵌套的方式寫另一個(gè)版本:
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestStringSliceEqual(t *testing.T) {
Convey("TestStringSliceEqual", t, func() {
Convey("should return true when a != nil && b != nil", func() {
a := []string{"hello", "goconvey"}
b := []string{"hello", "goconvey"}
So(StringSliceEqual(a, b), ShouldBeTrue)
})
Convey("should return true when a == nil && b == nil", func() {
So(StringSliceEqual(nil, nil), ShouldBeTrue)
})
Convey("should return false when a == nil && b != nil", func() {
a := []string(nil)
b := []string{}
So(StringSliceEqual(a, b), ShouldBeFalse)
})
Convey("should return false when a != nil && b != nil", func() {
a := []string{"hello", "world"}
b := []string{"hello", "goconvey"}
So(StringSliceEqual(a, b), ShouldBeFalse)
})
})
}
測(cè)試用例的執(zhí)行結(jié)果如下:
=== RUN TestStringSliceEqual
TestStringSliceEqual
should return true when a != nil && b != nil ?
should return true when a == nil && b == nil ?
should return false when a == nil && b != nil ?
should return false when a != nil && b != nil ?
4 total assertions
--- PASS: TestStringSliceEqual (0.00s)
PASS
ok infra/alg 0.006s
可見,Convey語句嵌套的測(cè)試日志和Convey語句不嵌套的測(cè)試日志的顯示有差異蛤铜,筆者更喜歡這種以測(cè)試函數(shù)為單位多個(gè)測(cè)試用例集中顯示的形式穆刻。
此外朵锣,Convey語句嵌套還有一種三層嵌套的慣用法泣刹,即按BDD風(fēng)格來寫測(cè)試用例掀泳,核心點(diǎn)是通過GWT(Given…When…Then)格式來描述測(cè)試用例马僻,示例如下:
func TestStringSliceEqualIfBothNotNil(t *testing.T) {
Convey("Given two string slice which are both not nil", t, func() {
a := []string{"hello", "goconvey"}
b := []string{"hello", "goconvey"}
Convey("When the comparision is done", func() {
result := StringSliceEqual(a, b)
Convey("Then the result should be true", func() {
So(result, ShouldBeTrue)
})
})
})
}
GWT測(cè)試用例的執(zhí)行結(jié)果如下:
=== RUN TestStringSliceEqualIfBothNotNil
Given two string slice which are both not nil
When the comparision is done
Then the result should be true ?
1 total assertion
--- PASS: TestStringSliceEqualIfBothNotNil (0.00s)
ok infra/alg 0.007s
按GWT格式寫測(cè)試用例時(shí),每一組GWT對(duì)應(yīng)一條測(cè)試用例擒权,即最內(nèi)層的Convey語句不像兩層嵌套時(shí)可以有多個(gè)瓣窄,而是只能有一個(gè)Convey語句裳凸。
我們依次寫出其余三個(gè)用例的三層嵌套形式:
func TestStringSliceEqualIfBothNil(t *testing.T) {
Convey("Given two string slice which are both nil", t, func() {
var a []string = nil
var b []string = nil
Convey("When the comparision is done", func() {
result := StringSliceEqual(a, b)
Convey("Then the result should be true", func() {
So(result, ShouldBeTrue)
})
})
})
}
func TestStringSliceNotEqualIfNotBothNil(t *testing.T) {
Convey("Given two string slice which are both nil", t, func() {
a := []string(nil)
b := []string{}
Convey("When the comparision is done", func() {
result := StringSliceEqual(a, b)
Convey("Then the result should be false", func() {
So(result, ShouldBeFalse)
})
})
})
}
func TestStringSliceNotEqualIfBothNotNil(t *testing.T) {
Convey("Given two string slice which are both not nil", t, func() {
a := []string{"hello", "world"}
b := []string{"hello", "goconvey"}
Convey("When the comparision is done", func() {
result := StringSliceEqual(a, b)
Convey("Then the result should be false", func() {
So(result, ShouldBeFalse)
})
})
})
}
我們?cè)賹⑸厦娴乃臈l用例使用測(cè)試套的形式來寫瓣颅,即一個(gè)測(cè)試函數(shù)包含多條用例粉怕,每條用例使用Convey語句四層嵌套的慣用法:
func TestStringSliceEqual(t *testing.T) {
Convey("TestStringSliceEqualIfBothNotNil", t, func() {
Convey("Given two string slice which are both not nil", func() {
a := []string{"hello", "goconvey"}
b := []string{"hello", "goconvey"}
Convey("When the comparision is done", func() {
result := StringSliceEqual(a, b)
Convey("Then the result should be true", func() {
So(result, ShouldBeTrue)
})
})
})
})
Convey("TestStringSliceEqualIfBothNil", t, func() {
Convey("Given two string slice which are both nil", func() {
var a []string = nil
var b []string = nil
Convey("When the comparision is done", func() {
result := StringSliceEqual(a, b)
Convey("Then the result should be true", func() {
So(result, ShouldBeTrue)
})
})
})
})
Convey("TestStringSliceNotEqualIfNotBothNil", t, func() {
Convey("Given two string slice which are both nil", func() {
a := []string(nil)
b := []string{}
Convey("When the comparision is done", func() {
result := StringSliceEqual(a, b)
Convey("Then the result should be false", func() {
So(result, ShouldBeFalse)
})
})
})
})
Convey("TestStringSliceNotEqualIfBothNotNil", t, func() {
Convey("Given two string slice which are both not nil", func() {
a := []string{"hello", "world"}
b := []string{"hello", "goconvey"}
Convey("When the comparision is done", func() {
result := StringSliceEqual(a, b)
Convey("Then the result should be false", func() {
So(result, ShouldBeFalse)
})
})
})
})
}
Web 界面
GoConvey不僅支持在命令行進(jìn)行自動(dòng)化編譯測(cè)試还绘,而且還支持在 Web 界面進(jìn)行自動(dòng)化編譯測(cè)試。想要使用GoConvey的 Web 界面特性又谋,需要在測(cè)試文件所在目錄下執(zhí)行g(shù)oconvey:
$GOPATH/bin/goconvey
這時(shí)彈出一個(gè)頁面娇斩,如下圖所示:
在 Web 界面中:
- 可以設(shè)置界面主題
- 查看完整的測(cè)試結(jié)果
- 使用瀏覽器提醒等實(shí)用功能
- 自動(dòng)檢測(cè)代碼變動(dòng)并編譯測(cè)試
- 半自動(dòng)化書寫測(cè)試用例
- 查看測(cè)試覆蓋率
- 臨時(shí)屏蔽某個(gè)包的編譯測(cè)試
Skip
針對(duì)想忽略但又不想刪掉或注釋掉某些斷言操作志珍,GoConvey提供了Convey/So的Skip方法:
- SkipConvey函數(shù)表明相應(yīng)的閉包函數(shù)將不被執(zhí)行
- SkipSo函數(shù)表明相應(yīng)的斷言將不被執(zhí)行
當(dāng)存在SkipConvey或SkipSo時(shí),測(cè)試日志中會(huì)顯式打上"skipped"形式的標(biāo)記:
- 當(dāng)測(cè)試代碼中存在SkipConvey時(shí)旁壮,相應(yīng)閉包函數(shù)中不管是否為SkipSo厦坛,都將被忽略放仗,測(cè)試日志中對(duì)應(yīng)的符號(hào)僅為一個(gè)"?"
- 當(dāng)測(cè)試代碼Convey語句中存在SkipSo時(shí)惶傻,測(cè)試日志中每個(gè)So對(duì)應(yīng)一個(gè)"?"或"?"励翼,每個(gè)SkipSo對(duì)應(yīng)一個(gè)"?",按實(shí)際順序排列
- 不管存在SkipConvey還是SkipSo時(shí),測(cè)試日志中都有字符串"{n} total assertions (one or more sections skipped)",其中{n}表示測(cè)試中實(shí)際已運(yùn)行的斷言語句數(shù)
定制斷言函數(shù)
我們先看一下So函數(shù)的聲明:
func So(actual interface{}, assert Assertion, expected ...interface{})
第二個(gè)參數(shù)為assert贩虾,是一個(gè)函數(shù)變量,它的類型Assertion的定義為:
type Assertion func(actual interface{}, expected ...interface{}) string
當(dāng)Assertion的變量的返回值為""時(shí)表示斷言成功考杉,否則表示失敳呔:
const assertionSuccess = ""
我們簡(jiǎn)單實(shí)現(xiàn)一個(gè)Assertion函數(shù):
func ShouldSummerBeComming(actual interface{}, expected ...interface{}) string {
if actual == "summer" && expected[0] == "comming" {
return ""
} else {
return "summer is not comming!"
}
}
我們?nèi)匀辉趕lice_test文件中寫一個(gè)簡(jiǎn)單測(cè)試:
func TestSummer(t *testing.T) {
Convey("TestSummer", t, func() {
So("summer", ShouldSummerBeComming, "comming")
So("winter", ShouldSummerBeComming, "comming")
})
}
根據(jù)ShouldSummerBeComming的實(shí)現(xiàn),Convey語句中第一個(gè)So將斷言成功奔则,第二個(gè)So將斷言失敗蛮寂。
我們運(yùn)行測(cè)試,查看執(zhí)行結(jié)果易茬,符合期望:
=== RUN TestSummer
TestSummer ??
Failures:
* /Users/zhangxiaolong/Desktop/D/go-workspace/src/infra/alg/slice_test.go
Line 52:
summer is not comming!
2 total assertions
--- FAIL: TestSummer (0.00s)
FAIL
exit status 1
FAIL infra/alg 0.006s
小結(jié)
Golang雖然自帶了單元測(cè)試功能,但筆者建議讀者使用已經(jīng)成熟的第三方測(cè)試框架及老。本文主要介紹了GoConvey框架抽莱,通過文字結(jié)合代碼示例講解基本的使用方法,要點(diǎn)歸納如下:
- import goconvey包時(shí)骄恶,前面加點(diǎn)號(hào)"."食铐,以減少冗余的代碼;
- 測(cè)試函數(shù)的名字必須以Test開頭僧鲁,而且參數(shù)類型必須為*testing.T虐呻;
- 每個(gè)測(cè)試用例必須使用Convey語句包裹起來,推薦使用Convey語句的嵌套寞秃,即一個(gè)函數(shù)有一個(gè)或多個(gè)測(cè)試函數(shù)斟叼,一個(gè)測(cè)試函數(shù)嵌套兩層、三層或四層Convey語句春寿;
- Convey語句的第三個(gè)參數(shù)習(xí)慣以閉包的形式實(shí)現(xiàn)朗涩,在閉包中通過So語句完成斷言;
- 使用GoConvey框架的 Web 界面特性绑改,作為命令行的補(bǔ)充谢床;
- 在適當(dāng)?shù)膱?chǎng)景下使用SkipConvey函數(shù)或SkipSo函數(shù)兄一;
- 當(dāng)測(cè)試中有需要時(shí),可以定制斷言函數(shù)识腿。
至此出革,希望讀者已經(jīng)掌握了GoConvey框架的基本用法,從而可以寫出簡(jiǎn)單優(yōu)雅的測(cè)試代碼渡讼。
然而骂束,事情并沒有這么簡(jiǎn)單!試想硝全,如果在被測(cè)函數(shù)中調(diào)用了底層rand包的Intn函數(shù)栖雾,你會(huì)如何寫測(cè)試代碼?經(jīng)過思考伟众,你應(yīng)該會(huì)發(fā)現(xiàn)需要給rand包的Intn函數(shù)打樁析藕。如何低成本的滿足用戶各種測(cè)試場(chǎng)景的打樁訴求,這正是GoMonkey框架的專長(zhǎng)凳厢。