閱讀本篇文章前,你最好已經(jīng)知道如何寫基本的單元測試。本篇文章共包含3個小建議,以及7個小技巧。
建議一,不要使用框架
Go語言自身已經(jīng)有一個非常棒的測試框架,它允許你使用Go編寫測試代碼,不需要再額外學(xué)習(xí)其它的庫或測試引擎。關(guān)于斷言方面的幫助函數(shù),你可以看看這個 testing,或者這個 assert.go :)
建議二,使用"_test"包名
相較于直接使用被測試代碼的包名,使用 *_test包名使得測試代碼只能訪問包中對外暴露出的接口。這使得你在寫測試時更多的是站在包使用者的角度來寫,從而使得你可以思考包的接口是否設(shè)計(jì)合理。
建議三,避免全局常量配置項(xiàng)
避免使用全局常量配置項(xiàng),因?yàn)闇y試代碼無法修改常量。下面舉了三個例子做對比:
// 1. 不好,測試代碼無法修改它
const port = 8080
// 2. 好一些,測試代碼可以修改它
var port = 8080
// 3. 更好的方式,測試代碼可以通過 struct 配置 Port
const defaultPort = 8080
type AppConfig {
Port int // 構(gòu)造函數(shù)中初始化為 defaultPort
}
技巧一,加載測試數(shù)據(jù)
Go對從文件中加載測試數(shù)據(jù)提供了非常好的支持。首先,Go編譯時會忽略testdata目錄。然后,當(dāng)測試代碼運(yùn)行時,Go會將當(dāng)前目錄作為包的目錄。這使得你可以使用相對路徑來訪問testdata目錄。看例子:
func helperLoadBytes(t *testing.T, name string) []byte {
path := filepath.Join("testdata", name) // relative path
bytes, err := ioutil.ReadFile(path)
if err != nil {
t.Fatal(err)
}
return bytes
}
技巧二,保存測試時的預(yù)期結(jié)果至.golden文件中
將測試時的預(yù)期結(jié)果保存至.golden文件中。并且提供一個flag來決定是否更新它。使用這個技巧可以避免在測試代碼中硬編碼預(yù)期輸出結(jié)果非常復(fù)雜的內(nèi)容??蠢樱?/p>
var update = flag.Bool("update", false, "update .golden files")
func TestSomething(t *testing.T) {
actual := doSomething()
golden := filepath.Join(“testdata”, tc.Name+”.golden”)
if *update {
ioutil.WriteFile(golden, actual, 0644)
}
expected, _ := ioutil.ReadFile(golden)
if !bytes.Equal(actual, expected) {
// FAIL!
}
}
技巧三,測試時的初始化、清理代碼
有時候測試代碼比較復(fù)雜,在跑測試的case之前需要初始化好環(huán)境,這可能會包含很多不相關(guān)的錯誤檢查,比如測試文件是否加載成功,測試數(shù)據(jù)是否能按json格式解析等等。這使得測試代碼變得很不純粹優(yōu)雅。
為了解決這個問題,你可以把不相關(guān)的代碼放入幫助函數(shù)中。這些函數(shù)永遠(yuǎn)不返回error,而是傳入*testing.T,當(dāng)有錯誤發(fā)生時直接斷言報(bào)錯。
同樣的,如果幫助函數(shù)需要在結(jié)束后做清理工作,幫助函數(shù)應(yīng)該返回一個函數(shù)做清理工作??蠢樱?/p>
func testChdir(t *testing.T, dir string) func() {
old, err := os.Getwd()
if err != nil {
t.Fatalf("err: %s", err)
}
if err := os.Chdir(dir); err != nil {
t.Fatalf("err: %s", err)
}
return func() { // 返回清理函數(shù),供外部需要清理時調(diào)用
if err := os.Chdir(old); err != nil {
t.Fatalf("err: %s", err)
}
}
}
func TestThing(t *testing.T) {
defer testChdir(t, "/other")()
// ...
}
上面的例子包含了另外一個關(guān)于defer使用的非??岬募记?。defer testChdir(t, "/other")()會先執(zhí)行testChdir內(nèi)的代碼,并且在TestThing結(jié)束時執(zhí)行testChdir所返回的清理函數(shù)中的代碼。
技巧四,當(dāng)依賴第三方可執(zhí)行程序時
有時測試代碼會依賴第三方可執(zhí)行程序,我們可以通過以下方法檢查程序是否存在,存在則執(zhí)行測試,不存在則跳過測試。
var testHasGit bool
func init() {
if _, err := exec.LookPath("git"); err == nil {
testHasGit = true
}
}
func TestGitGetter(t *testing.T) {
if !testHasGit {
t.Log("git not found, skipping")
t.Skip()
}
// ...
}
技巧五,測試包含os.Exit的代碼
該方法通過啟動子進(jìn)程的方式,避免測試包含os.Exit的代碼導(dǎo)致測試程序提前退出。看例子:
func CrashingGit() {
os.Exit(1)
}
func TestFailingGit(t *testing.T) {
if os.Getenv("BE_CRASHING_GIT") == "1" { // 子進(jìn)程進(jìn)入這個邏輯分支
CrashingGit()
return
}
// 被 go test 執(zhí)行,
// 設(shè)置好環(huán)境變量,啟動子進(jìn)程再次執(zhí)行 TestFailingGit
cmd := exec.Command(os.Args[0], "-test.run=TestFailingGit")
cmd.Env = append(os.Environ(), "BE_CRASHING_GIT=1")
err := cmd.Run()
if e, ok := err.(*exec.ExitError); ok && !e.Success() {
return
}
t.Fatalf("Process ran with err %v, want os.Exit(1)", err)
}
上面例子的思想是,當(dāng)Go測試框架運(yùn)行TestFailingGit時,啟動一個子進(jìn)程(os.Args[0]即生成的Go測試程序)。子進(jìn)程再次運(yùn)行測試程序,并只執(zhí)行TestFailingGit(通過參數(shù) -test.run=TestFailingGit 實(shí)現(xiàn)),并且設(shè)置了環(huán)境變量BE_CRASHING_GIT=1,這樣子進(jìn)程將執(zhí)行CrashingGit()。
技巧六,將mocks、helpers放入testing.go文件中
testing.go文件會被當(dāng)做一個普通的源碼文件,而不是測試代碼文件。這樣在其它的包中或其它包的測試代碼可以使用這些mocks、helpers。
技巧七,單獨(dú)處理耗時長的測試
當(dāng)存在一些耗時很長的測試時,等待所有的測試結(jié)束會讓人煩躁。解決方法是將這些耗時長的測試放入_integration_test.go文件中,并在該文件的頭部加入編譯tag??蠢樱?/p>
// +build integration
這樣Go測試時默認(rèn)不會運(yùn)行這些測試代碼。
如果想運(yùn)行所有的測試代碼,你可以這樣:
go test -tags=integration
以下是我個人使用alias做的一個簡便命令,可以運(yùn)行當(dāng)前目錄以及子目錄中除vendoer目錄外的所有測試:
alias gtest="go test \$(go list ./… | grep -v /vendor/) -tags=integration"
這個命令可以配合-v參數(shù)使用:
$ gtest
…
$ gtest -v
…
感謝閱讀,英文原文地址:Go advanced testing tips & tricks (https://medium.com/@povilasve/go-advanced-tips-tricks-a872503ac859)
- 本文作者:yoko
- 本文鏈接:http://www.pengrl.com/p/32101/
- 版權(quán)聲明:本博客所有文章除特別聲明外,均采用 CC BY-NC-SA 3.0 許可協(xié)議。轉(zhuǎn)載請注明出處!