Ginkgo:一款 BDD 的 Go 語(yǔ)言框架
在 如何有效地測(cè)試Go代碼 一文中,我們談?wù)摿藛卧獪y(cè)試,針對(duì)它的兩大難點(diǎn):解耦、依賴,提出了 面向接口、mock 依賴 的解決方案。同時(shí),該文還討論了一些 Go 領(lǐng)域內(nèi)的實(shí)用測(cè)試工具,歡迎讀者閱讀。
單元測(cè)試關(guān)注點(diǎn)是代碼邏輯單元,一般是一個(gè)對(duì)象或者一個(gè)具體函數(shù)。我們可以編寫足夠的單元測(cè)試來(lái)確保代碼的質(zhì)量,當(dāng)功能修改或代碼重構(gòu)時(shí),充分的單元測(cè)試案例能夠給予我們足夠的信心。
單元測(cè)試之上是開(kāi)發(fā)規(guī)范。在敏捷軟件開(kāi)發(fā)中,有兩位??停簻y(cè)試驅(qū)動(dòng)開(kāi)發(fā)(Test-Driven Development,TDD)和行為驅(qū)動(dòng)開(kāi)發(fā)(Behavior-driven development,BDD)。它們是實(shí)踐與技術(shù),同時(shí)也是設(shè)計(jì)方法論。
TDD
TDD 的基本思路就是通過(guò)測(cè)試來(lái)推動(dòng)整個(gè)開(kāi)發(fā)的進(jìn)行,原則就是在開(kāi)發(fā)功能代碼之前,先編寫單元測(cè)試用例。包含以下五個(gè)步驟:
開(kāi)發(fā)者首先寫一些測(cè)試用例 運(yùn)行這些測(cè)試,但這些測(cè)試明顯都會(huì)失敗,因?yàn)闇y(cè)試用例中的業(yè)務(wù)邏輯還沒(méi)實(shí)現(xiàn) 實(shí)現(xiàn)代碼細(xì)節(jié) 如果開(kāi)發(fā)者順利實(shí)現(xiàn)代碼的話,運(yùn)行所有測(cè)試就會(huì)通過(guò) 對(duì)業(yè)務(wù)代碼及時(shí)重構(gòu),如果新代碼功能不正確的話,對(duì)應(yīng)的測(cè)試文件也會(huì)失敗
當(dāng)需要開(kāi)發(fā)新功能時(shí),重復(fù)上述步驟。流程如下圖所示

有一個(gè) Github 倉(cāng)庫(kù)比較有趣:learn-go-with-tests ,該倉(cāng)庫(kù)旨在通過(guò) Go 學(xué)習(xí) TDD 。
BDD
TDD 側(cè)重點(diǎn)偏向開(kāi)發(fā),通過(guò)測(cè)試用例來(lái)規(guī)范約束開(kāi)發(fā)者編寫出質(zhì)量更高、bug更少的代碼。而 BDD更加側(cè)重設(shè)計(jì),其要求在設(shè)計(jì)測(cè)試用例時(shí)對(duì)系統(tǒng)進(jìn)行定義,倡導(dǎo)使用通用的語(yǔ)言將系統(tǒng)的行為描述出來(lái),將系統(tǒng)設(shè)計(jì)和測(cè)試用例結(jié)合起來(lái),以此為驅(qū)動(dòng)進(jìn)行開(kāi)發(fā)工作。
BDD 衍生于 TDD,主要區(qū)別就是在于測(cè)試的描述上。BDD 使用一種更通俗易懂的文字來(lái)描述測(cè)試用例,更關(guān)注需求的功能,而不是實(shí)際結(jié)果。
BDD 賦予的像閱讀句子一樣閱讀測(cè)試的能力帶來(lái)對(duì)測(cè)試認(rèn)知上的轉(zhuǎn)變,有助于我們?nèi)タ紤]如何更好寫測(cè)試。
Ginkgo
Ginkgo 是一個(gè) Go 語(yǔ)言的 BDD 測(cè)試框架,旨在幫助開(kāi)發(fā)者編寫富有表現(xiàn)力的全方位測(cè)試。
Ginkgo 集成了 Go 原生的 testing 庫(kù),這意味著你可以通過(guò) go test 來(lái)運(yùn)行 Ginkgo 測(cè)試套件。同時(shí),它與斷言和 mock 套件 testify 、富測(cè)試集 go-check 同樣兼容。但 Ginkgo 建議的是搭配 gomega 庫(kù)一起使用。
下面,我們使用 Ginkgo 來(lái)感受一下 BDD 模式的測(cè)試代碼。
下載
使用 go get 獲取
$ go get github.com/onsi/ginkgo/ginkgo
$ go get github.com/onsi/gomega/...
該命令獲取 ginkgo 并安裝 ginkgo 可執(zhí)行文件到 $GOPATH/bin 。
創(chuàng)建套件
創(chuàng)建 gopher 庫(kù)
$ cd path-to-package/gopher
在 gopher.go 文件中,有 Gopher 結(jié)構(gòu)體與校驗(yàn)方法 Validate 如下
package gopher
import (
"errors"
"unicode/utf8"
)
type Gopher struct {
Name string
Gender string
Age int
}
func Validate(g Gopher) error {
if utf8.RuneCountInString(g.Name) < 3 {
return errors.New("名字太短,不能小于3")
}
if g.Gender != "男" {
return errors.New("只要男的")
}
if g.Age < 18 {
return errors.New("歲數(shù)太小,不能小于18")
}
return nil
}
我們通過(guò) ginkgo bootstrap 命令,來(lái)初始化一個(gè) Ginkgo 測(cè)試套件。
$ ginkgo bootstrap
Generating ginkgo test suite bootstrap for gopher in:
gopher_suite_test.go
此時(shí)在 gopher.go 同級(jí)目錄中,生成了 gopher_suite_test.go 文件,內(nèi)容如下
package gopher_test
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestGopher(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Gopher Suite")
}
此時(shí),我們就可以運(yùn)行測(cè)試套件了,通過(guò)命令 go test 或 ginkgo 均可。
$ go test
Running Suite: Gopher Suite
===========================
Random Seed: 1629621653
Will run 0 of 0 specs
Ran 0 of 0 Specs in 0.000 seconds
SUCCESS! -- 0 Passed | 0 Failed | 0 Pending | 0 Skipped
PASS
ok ginkgo/gopher 0.018s
當(dāng)然,空測(cè)試套件沒(méi)有什么價(jià)值,我們需要在此套件下編寫測(cè)試(Spec)用例。
我們可以在 gopher_suite_test.go 中編寫測(cè)試,但是推薦分離到獨(dú)立的文件中,特別是包中有多個(gè)需要被測(cè)試的源文件的情況下。
創(chuàng)建 Spec
執(zhí)行 ginkgo generate gopher 可以生成一個(gè) gopher_test.go 測(cè)試文件。
$ ginkgo generate gopher
Generating ginkgo test for Gopher in:
gopher_test.go
此時(shí)測(cè)試文件中的內(nèi)容如下
package gopher_test
import (
. "github.com/onsi/ginkgo"
)
var _ = Describe("Gopher", func() {
})
編寫 Spec
我們基于此測(cè)試文件撰寫實(shí)際的測(cè)試用例
package gopher_test
import (
"ginkgo/gopher"
. "github.com/onsi/ginkgo"
"github.com/onsi/gomega"
)
func mockInputData() ([]gopher.Gopher, error) {
inputData := []gopher.Gopher{
{
Name: "菜刀",
Gender: "男",
Age: 18,
},
{
Name: "小西瓜",
Gender: "女",
Age: 19,
},
{
Name: "機(jī)器鈴砍菜刀",
Gender: "男",
Age: 17,
},
{
Name: "小菜刀",
Gender: "男",
Age: 20,
},
}
return inputData, nil
}
var _ = Describe("Gopher", func() {
BeforeEach(func() {
By("當(dāng)測(cè)試不通過(guò)時(shí),我會(huì)在這里打印一個(gè)消息 【BeforeEach】")
})
inputData, err := mockInputData()
Describe("校驗(yàn)輸入數(shù)據(jù)", func() {
Context("當(dāng)獲取數(shù)據(jù)沒(méi)有錯(cuò)誤發(fā)生時(shí)", func() {
It("它應(yīng)該是接收數(shù)據(jù)成功了的", func() {
gomega.Expect(err).Should(gomega.BeNil())
})
})
Context("當(dāng)獲取的數(shù)據(jù)校驗(yàn)失敗時(shí)", func() {
It("當(dāng)數(shù)據(jù)校驗(yàn)返回錯(cuò)誤為:名字太短,不能小于3 時(shí)", func() {
gomega.Expect(gopher.Validate(inputData[0])).Should(gomega.MatchError("名字太短,不能小于3"))
})
It("當(dāng)數(shù)據(jù)校驗(yàn)返回錯(cuò)誤為:只要男的 時(shí)", func() {
gomega.Expect(gopher.Validate(inputData[1])).Should(gomega.MatchError("只要男的"))
})
It("當(dāng)數(shù)據(jù)校驗(yàn)返回錯(cuò)誤為:歲數(shù)太小,不能小于18 時(shí)", func() {
gomega.Expect(gopher.Validate(inputData[2])).Should(gomega.MatchError("歲數(shù)太小,不能小于18"))
})
})
Context("當(dāng)獲取的數(shù)據(jù)校驗(yàn)成功時(shí)", func() {
It("通過(guò)了數(shù)據(jù)校驗(yàn)", func() {
gomega.Expect(gopher.Validate(inputData[3])).Should(gomega.BeNil())
})
})
})
AfterEach(func() {
By("當(dāng)測(cè)試不通過(guò)時(shí),我會(huì)在這里打印一個(gè)消息 【AfterEach】")
})
})
可以看到,BDD 風(fēng)格的測(cè)試案例在代碼中就被描述地非常清晰。由于我們的測(cè)試用例與預(yù)期相符,執(zhí)行 go test 執(zhí)行測(cè)試套件會(huì)校驗(yàn)通過(guò)。
$ go test
Running Suite: Gopher Suite
===========================
Random Seed: 1629625854
Will run 5 of 5 specs
?????
Ran 5 of 5 Specs in 0.000 seconds
SUCCESS! -- 5 Passed | 0 Failed | 0 Pending | 0 Skipped
PASS
ok ginkgo/gopher 0.013s
讀者可自行更改數(shù)據(jù)致測(cè)試不通過(guò),你會(huì)看到 Ginkgo 將打印出堆棧與錯(cuò)誤描述性信息。
總結(jié)
TDD 和 BDD 是敏捷開(kāi)發(fā)中常被提到的方法論。與TDD相比,BDD 通過(guò)編寫 行為和規(guī)范 來(lái)驅(qū)動(dòng)軟件開(kāi)發(fā)。這些行為和規(guī)范在代碼中體現(xiàn)于更 ”繁瑣“ 的描述信息。
關(guān)于 BDD 的本質(zhì),有另外一種表達(dá)方式:BDD 幫助開(kāi)發(fā)人員設(shè)計(jì)軟件,TDD 幫助開(kāi)發(fā)人員測(cè)試軟件。
Ginkgo 是 Go 語(yǔ)言中非常優(yōu)秀的 BDD 框架,它通過(guò) DSL 語(yǔ)法(Describe/Context/It)有效地幫助開(kāi)發(fā)者組織與編排測(cè)試用例。本文只是展示了 Ginkgo 非常簡(jiǎn)單的用例,權(quán)當(dāng)是拋磚引玉。
讀者在使用 Ginkgo 過(guò)程中,需要理解它的執(zhí)行生命周期, 重點(diǎn)包括 It、Context、Describe、BeforeEach、AfterEach、JustBeforeEach、BeforeSuite、AfterSuite、By、Fail 這些模塊的執(zhí)行順序與語(yǔ)義邏輯。
Ginkgo 有很多的功能本文并未涉及,例如異步測(cè)試、基準(zhǔn)測(cè)試、持續(xù)集成等強(qiáng)大的支持。其倉(cāng)庫(kù)位于 https://github.com/onsi/ginkgo ,同時(shí)提供了英文版與中文版使用文檔,讀者可以借此了解更多 Ginkgo 信息。
最后,K8s 項(xiàng)目中也使用了 Ginkgo 框架,用于編寫其端到端 (End to End,E2E) 測(cè)試用例,值得借鑒學(xué)習(xí)。
------------------- End -------------------
往期精彩文章推薦:

歡迎大家點(diǎn)贊,留言,轉(zhuǎn)發(fā),轉(zhuǎn)載,感謝大家的相伴與支持
想加入Go學(xué)習(xí)群請(qǐng)?jiān)诤笈_(tái)回復(fù)【入群】
萬(wàn)水千山總是情,點(diǎn)個(gè)【在看】行不行
