Go 語言中的測試

接觸過 java 的同時,junit 這個名稱並不會陌生,或者 python 的測試框架 unittest。可是有習慣寫測試程序的卻聊聊無幾。在 golang 中,關於 testing 的支持比較完善,對於一般的函數測試來說,用不到第三方工具。

官方說明, golang testing https://golang.org/pkg/testing/

要在 go 程序開發過程中使用單元測試程序,無須安裝其它工具,安裝了 golang 環境時已經包含,golang 的工具鏈還是比較完善的。

關於單元測試代碼、測試用例、自動化測試等不是有悟想討論的,不同規模的工程、不同進度的項目要求、不同的管理方法,會讓管理者作出不同的決定。本文僅說說如果在 golang 程序中集成可測試代碼。

要想在 golang 使用 go test 工具鏈來實現代碼單元測試功能,只需要遵守 go test 工具的規範。

本文並未涉及的 golang testing 的所有內容,只會講有悟在寫測試程序用到的功能,完整內容見上面的官方說明鏈接。

以下從如何使用的角度來介紹 go testing,並不是大而全的框架性講解。

按照有悟的慣例,先準備一下示例代碼工程:

~/Projects/go/examples
➜ mkdir hellotest && cd hellotest

~/Projects/go/examples/hellotest
➜ go mod init hellotest

~/Projects/go/examples/hellotest
➜ touch youwutoday.go youwu_test.go
// youwutoday.go
package main

import (
    "fmt"
)

func main() {
    fmt.Println("hello testing...😀")
}
// youwu_test.go
package main_test

組織你的單元測試代碼

爲了更好的管理單元測試代碼,按照 go testing 的命名規範,運行 go testing 會掃描並編譯運行工程目錄下 xxx_test.go 代碼文件中的 TestXXX 測試函數。對一個沒有任何測試代碼的 go 程序工程運行 go testing 只會提示 no tests to run 並不會報錯。

不用擔心這些測試代碼管理會讓你的代碼工程變得混亂,你可以使用一個專門的工程級目錄(比如叫 test) 來存放所有主要的測試代碼。也可以在對應的包(比如 app)下使用對應測試包(比如 app_test)。這樣就可以利用人爲區分,用何種目錄方式完全取決於你的需要。但這對於 go 工具鏈來說不是必要的,因爲它並不會把這些測試代碼打包到程序的發行版本中。

關於 go testing 啓動命令

在上面的例子中,單元測試用例的啓動都是使用 go test 來啓動的,關於這個命令,有必要說明的地方。

go test .

僅從當前目錄查找 xxx_test.go 測試代碼

go test ./...

找出當前的目錄以及子目錄下所的 xxx_test.go 測試代碼

go test ./xxx/xxx

找出路徑下的 xxx_test.go 測試代碼,如果包含子目錄,使用 ./xxx/xxx/...,即在路徑後加上 /...,注意,是 3 個 “.” 號。

go test 路徑 -v

-v 參數,打開終端信息打印的開關。在沒有 -v 選項時,測試代碼中的打印函數(t.Log、t.Skip、t.Error、t.Fatal)向終端打印的信息,只有測試函數狀態爲 FAIL 的纔會顯示。而 fmt.Print 只會在任一測試函數爲 FAIL 時,信息纔會顯示。所以在調試單元測試代碼時,通常會使用 -v 參數,可多次成功執行的,不帶 -v,以保持終端信息提示的清晰。這只是實踐建議,是否使用取決於你。

go test -run ^TestXXXX$

-run 用來指定執行某個 TestXXX 單元測試函數,支持使用正則表達式來匹配測試函數名稱。當然,像 TestMain、init() 這類框架性代碼仍然會被執行,以確保環境正確。

在測試中報錯與跳出

在 go testing 中,你可能見不到 assert。go testing 框架簡化了這一切的工作。在上面的例子,有悟使用了 t.Log 來打印終端信息。你可以使用 t.Error 來結合 if 判斷來打印信息,並標記這個測試出現錯誤。

// youwu_test.go
func TestReportError(t *testing.T) {
    t.Error("report in: t.Error")
}

會得到類似於下面這樣的錯誤報告:

...
=== RUN   TestReportError
    youwu_test.go:42: report in: t.Error
--- FAIL: TestReportError (0.00s)
...
exit status 1
FAIL    hellotest       0.498s

其實 t.Error 等同於 t.Log + t.Fail,當只標記錯誤不打印信息時,使用t.Fail。

跳出單元測試函數的選擇:

t.SkipNow

跳出當前單元測試函數,狀態爲跳出(SKIP,不計爲失敗),執行下一個測試函數

t.Skip(信息)

t.Log -> t.SkipNow 打印並跳出當前單元測試函數

t.Fail

標記當前單元測試函數狀態爲錯誤(FAILED),繼續執行

t.FailNow

標記當前單元測試函數狀態爲錯誤(FAILED)並跳出,執行下一個測試函數

關於報錯機制,有如下幾種:

t.Error(信息)

t.Log(信息) -> t.Fail(),該單元測試會繼續執行

t.Fatal(信息)

t.Log(信息) -> t.FailNow(),跳出該單元測試,執行下一個單元測試函數

以上關於終端打印的函數 t.Log、t.Error、t.Fatal 都有對應的格式化形式 t.Logf、t.Errorf、t.Fatalf,它們的關係就像 fmt.Print 與 fmt.Printf 的關係一樣。

測試程序初始化或清理

類似於 java junit 或者 python unittest,單元測試程序的運行分爲 Setup、Run、tearDown三個步驟,其中 setup 是用爲 Run 提供環境初始化的過程,比如連接數據庫、數據準備等等,tearDown 則是 Run 之後清理,比如刪除臨時文件、數據庫關閉等操作。

在 go testing 測試框架中並沒命名 Setup、tearDown 函數。當然,你可以利用 go 程序本身的機制或者 go testing 提供的手段。

當然,如果你的單元測試代碼僅僅是一個無須外部數據支持的計算函數,那你根本無須理會初始化或者清理,直接將單元測試代碼編寫爲 xxx_test.go 中 TestXXX 函數即可。

任何 go 代碼程序運行時,當前包、當前代碼文件中的 init() 函數會被最先執行。xxx_test.go 測試代碼的也不例外,那麼可以將初始化代碼放在這個函數內部。

(一個包內有多個初始化函數時,會按一定順序執行。將在另一篇文件中介紹 )

在 youwu_test.go 中添加

package main_test

import (
    "fmt"
    "testing"
)
func TestWorld(t *testing.T) {
    t.Log("TestWorlds")
}
func init() {
    fmt.Println("hello, i'm \"init\".")
}

運行得到的結果如下:

~/Projects/go/examples/hellotest 
➜  go test -v
hello, i'm "init".
=== RUN   TestHello
    youwu_test.go:9: TestHello
--- PASS: TestHello (0.00s)
PASS
ok      hellotest       0.316s

測試函數級別:使用 t 實例的 Cleanup 方法註冊一個在單元測試程序退出前執行的函數。包級別:使用 TestMain 模式。

func cleanuptest() {
    fmt.Println("Cleanup.😀")
}
func TestClean(t *testing.T) {
    t.Cleanup(cleanuptest)
    t.Log("testing done.")
}

運行得到的結果如下:

~/Projects/go/examples/hellotest 
➜  go test -v
hello, i'm "init".
=== RUN   TestClean
    youwu_test.go:21: testing done.
Cleanup.😀
--- PASS: TestClean (0.00s)
...

TestMain 是 go testing 測試框架的指定函數。用於控制整個測試過程,這個函數是包級別的。即一個包下,如果有多個 xxx_test.go 測試代碼,只能在其中某個 xxx_test.go 中定義。其基本格式大致如下:

在測試代碼中添加:

// youwu_test.go
package main_test

import (
    "fmt"
    "os"
    "testing"
)

func TestHello(t *testing.T) {
    t.Log("TestHello")
}

// func TestWorld(t *testing.T) {
//  t.Log("TestWorlds")
// }

func cleanuptest() {
    fmt.Println("Cleanup.😀")
}
func TestClean(t *testing.T) {
    t.Cleanup(cleanuptest)
    t.Log("testing done.")
}

func init() {
    fmt.Println("hello, i'm \"init\".")
}

func TestMain(m *testing.M) {
    // 在開始運行單元測試代碼之前
    // 可以在此處添加環境初始化相關代碼或者函數調用
    fmt.Println("😀 開始所有測試前")

    retCode := m.Run()

    // 在全部測試代碼運行結束退出之前
    // 可以在此處添加清理代碼或函數調用
    fmt.Println("😀 結束所有測試前")

    os.Exit(retCode)
}

使用 go test -v 運行得到:

~/Projects/go/examples/hellotest 
➜  go test -v
hello, i'm "init".
😀 開始所有測試前
=== RUN   TestHello
    youwu_test.go:10: TestHello
--- PASS: TestHello (0.00s)
=== RUN   TestClean
    youwu_test.go:22: testing done.
Cleanup.😀
--- PASS: TestClean (0.00s)
PASS
😀 結束所有測試前
ok      hellotest       0.369s

以上的順序,可以概括爲如下的流程:

`init()` → `TestMain` → TestXXX
                           ↓  
              ↑       ← t.Cleanup

benckmark | 性能基準測試

go testing 除了函數正確性測試之外,還支持性能基準測試(benchmark),即可多次運行,並計算其平均執行時間從而評估其運行效率。網上一些開源的 go 項目經常會擺出一些與同類工具的性能基準測試對比,就是使用 go testing benchmark 輕鬆做出來的。

性能基準測試的代碼命名規範與普通 testing 代碼的要求類似,除部分性能基準測試專有的函數外,其它像報告打印、跳出機制都一樣。不過由於 benchmark 程序會多次執行,除非有必要,在測試函數中儘量少用打印函數。

go 性能基準測試代碼的函數命名:

func BenchmarkXxx(*testing.T)

這些基準測試代碼可以與普通的單元測試代碼放在一起。通常使用 go test 不會進行基準測試,需要添加參數。

還是 youwu_test.go

// youwu_test.go
package main_test

import (
    "fmt"
    "math/rand"
    "os"
    "testing"
)

func TestHello(t *testing.T) {
    t.Log("TestHello")
}

func init() {
    fmt.Println("hello, i'm \"init\".")
}

func TestMain(m *testing.M) {
    // 在開始運行單元測試代碼之前
    // 可以在此處添加環境初始化相關代碼或者函數調用
    fmt.Println("😀 開始所有測試前")

    retCode := m.Run()

    // 在全部測試代碼運行結束退出之前
    // 可以在此處添加清理代碼或函數調用
    fmt.Println("😀 結束所有測試前")

    os.Exit(retCode)
}

func BenchmarkRandInt(b *testing.B) {
    for i := 0; i < b.N; i++ {
        rand.Int()
    }
}

使用命令 go test -bench='Rand' -v .得到的結果如下:

~/Projects/go/examples/hellotest 
➜  go test -bench='Rand' -v .
hello, i'm "init".
😀 開始所有測試前
=== RUN   TestHello
    youwu_test.go:19: TestHello
--- PASS: TestHello (0.00s)
goos: darwin
goarch: amd64
pkg: hellotest
cpu: Intel(R) Core(TM) i5-7500 CPU @ 3.40GHz
BenchmarkRandInt
BenchmarkRandInt-4      70186930                16.21 ns/op
PASS
😀 結束所有測試前
ok      hellotest       1.476s

可見,普通的單元測試代碼還是會被執行。使用 -bench='Rand' 參數來指定 go testing 執行 BenchmarkRandInt 這個函數。終端報告中的

BenchmarkRandInt-4      70186930                16.21 ns/op

表示運行了 70186930 次,每次 16.21 納秒。更多支持請看官方文檔,或者命令 go help test 查看關於 benchmark 部分。【導讀】阿斯蒂芬

接觸過 java 的同時,junit 這個名稱並不會陌生,或者 python 的測試框架 unittest。可是有習慣寫測試程序的卻聊聊無幾。在 golang 中,關於 testing 的支持比較完善,對於一般的函數測試來說,用不到第三方工具。

官方說明, golang testing https://golang.org/pkg/testing/

要在 go 程序開發過程中使用單元測試程序,無須安裝其它工具,安裝了 golang 環境時已經包含,golang 的工具鏈還是比較完善的。

關於單元測試代碼、測試用例、自動化測試等不是有悟想討論的,不同規模的工程、不同進度的項目要求、不同的管理方法,會讓管理者作出不同的決定。本文僅說說如果在 golang 程序中集成可測試代碼。

要想在 golang 使用 go test 工具鏈來實現代碼單元測試功能,只需要遵守 go test 工具的規範。

本文並未涉及的 golang testing 的所有內容,只會講有悟在寫測試程序用到的功能,完整內容見上面的官方說明鏈接。

以下從如何使用的角度來介紹 go testing,並不是大而全的框架性講解。

按照有悟的慣例,先準備一下示例代碼工程:

~/Projects/go/examples
➜ mkdir hellotest && cd hellotest

~/Projects/go/examples/hellotest
➜ go mod init hellotest

~/Projects/go/examples/hellotest
➜ touch youwutoday.go youwu_test.go
// youwutoday.go
package main

import (
    "fmt"
)

func main() {
    fmt.Println("hello testing...😀")
}
// youwu_test.go
package main_test

組織你的單元測試代碼

爲了更好的管理單元測試代碼,按照 go testing 的命名規範,運行 go testing 會掃描並編譯運行工程目錄下 xxx_test.go 代碼文件中的 TestXXX 測試函數。對一個沒有任何測試代碼的 go 程序工程運行 go testing 只會提示 no tests to run 並不會報錯。

不用擔心這些測試代碼管理會讓你的代碼工程變得混亂,你可以使用一個專門的工程級目錄(比如叫 test) 來存放所有主要的測試代碼。也可以在對應的包(比如 app)下使用對應測試包(比如 app_test)。這樣就可以利用人爲區分,用何種目錄方式完全取決於你的需要。但這對於 go 工具鏈來說不是必要的,因爲它並不會把這些測試代碼打包到程序的發行版本中。

關於 go testing 啓動命令

在上面的例子中,單元測試用例的啓動都是使用 go test 來啓動的,關於這個命令,有必要說明的地方。

go test .

僅從當前目錄查找 xxx_test.go 測試代碼

go test ./...

找出當前的目錄以及子目錄下所的 xxx_test.go 測試代碼

go test ./xxx/xxx

找出路徑下的 xxx_test.go 測試代碼,如果包含子目錄,使用 ./xxx/xxx/...,即在路徑後加上 /...,注意,是 3 個 “.” 號。

go test 路徑 -v

-v 參數,打開終端信息打印的開關。在沒有 -v 選項時,測試代碼中的打印函數(t.Log、t.Skip、t.Error、t.Fatal)向終端打印的信息,只有測試函數狀態爲 FAIL 的纔會顯示。而 fmt.Print 只會在任一測試函數爲 FAIL 時,信息纔會顯示。所以在調試單元測試代碼時,通常會使用 -v 參數,可多次成功執行的,不帶 -v,以保持終端信息提示的清晰。這只是實踐建議,是否使用取決於你。

go test -run ^TestXXXX$

-run 用來指定執行某個 TestXXX 單元測試函數,支持使用正則表達式來匹配測試函數名稱。當然,像 TestMain、init() 這類框架性代碼仍然會被執行,以確保環境正確。

在測試中報錯與跳出

在 go testing 中,你可能見不到 assert。go testing 框架簡化了這一切的工作。在上面的例子,有悟使用了 t.Log 來打印終端信息。你可以使用 t.Error 來結合 if 判斷來打印信息,並標記這個測試出現錯誤。

// youwu_test.go
func TestReportError(t *testing.T) {
    t.Error("report in: t.Error")
}

會得到類似於下面這樣的錯誤報告:

...
=== RUN   TestReportError
    youwu_test.go:42: report in: t.Error
--- FAIL: TestReportError (0.00s)
...
exit status 1
FAIL    hellotest       0.498s

其實 t.Error 等同於 t.Log + t.Fail,當只標記錯誤不打印信息時,使用t.Fail。

跳出單元測試函數的選擇:

t.SkipNow

跳出當前單元測試函數,狀態爲跳出(SKIP,不計爲失敗),執行下一個測試函數

t.Skip(信息)

t.Log -> t.SkipNow 打印並跳出當前單元測試函數

t.Fail

標記當前單元測試函數狀態爲錯誤(FAILED),繼續執行

t.FailNow

標記當前單元測試函數狀態爲錯誤(FAILED)並跳出,執行下一個測試函數

關於報錯機制,有如下幾種:

t.Error(信息)

t.Log(信息) -> t.Fail(),該單元測試會繼續執行

t.Fatal(信息)

t.Log(信息) -> t.FailNow(),跳出該單元測試,執行下一個單元測試函數

以上關於終端打印的函數 t.Log、t.Error、t.Fatal 都有對應的格式化形式 t.Logf、t.Errorf、t.Fatalf,它們的關係就像 fmt.Print 與 fmt.Printf 的關係一樣。

測試程序初始化或清理

類似於 java junit 或者 python unittest,單元測試程序的運行分爲 Setup、Run、tearDown三個步驟,其中 setup 是用爲 Run 提供環境初始化的過程,比如連接數據庫、數據準備等等,tearDown 則是 Run 之後清理,比如刪除臨時文件、數據庫關閉等操作。

在 go testing 測試框架中並沒命名 Setup、tearDown 函數。當然,你可以利用 go 程序本身的機制或者 go testing 提供的手段。

當然,如果你的單元測試代碼僅僅是一個無須外部數據支持的計算函數,那你根本無須理會初始化或者清理,直接將單元測試代碼編寫爲 xxx_test.go 中 TestXXX 函數即可。

任何 go 代碼程序運行時,當前包、當前代碼文件中的 init() 函數會被最先執行。xxx_test.go 測試代碼的也不例外,那麼可以將初始化代碼放在這個函數內部。

(一個包內有多個初始化函數時,會按一定順序執行。將在另一篇文件中介紹 )

在 youwu_test.go 中添加

package main_test

import (
    "fmt"
    "testing"
)
func TestWorld(t *testing.T) {
    t.Log("TestWorlds")
}
func init() {
    fmt.Println("hello, i'm \"init\".")
}

運行得到的結果如下:

~/Projects/go/examples/hellotest 
➜  go test -v
hello, i'm "init".
=== RUN   TestHello
    youwu_test.go:9: TestHello
--- PASS: TestHello (0.00s)
PASS
ok      hellotest       0.316s

測試函數級別:使用 t 實例的 Cleanup 方法註冊一個在單元測試程序退出前執行的函數。包級別:使用 TestMain 模式。

func cleanuptest() {
    fmt.Println("Cleanup.😀")
}
func TestClean(t *testing.T) {
    t.Cleanup(cleanuptest)
    t.Log("testing done.")
}

運行得到的結果如下:

~/Projects/go/examples/hellotest 
➜  go test -v
hello, i'm "init".
=== RUN   TestClean
    youwu_test.go:21: testing done.
Cleanup.😀
--- PASS: TestClean (0.00s)
...

TestMain 是 go testing 測試框架的指定函數。用於控制整個測試過程,這個函數是包級別的。即一個包下,如果有多個 xxx_test.go 測試代碼,只能在其中某個 xxx_test.go 中定義。其基本格式大致如下:

在測試代碼中添加:

// youwu_test.go
package main_test

import (
    "fmt"
    "os"
    "testing"
)

func TestHello(t *testing.T) {
    t.Log("TestHello")
}

// func TestWorld(t *testing.T) {
//  t.Log("TestWorlds")
// }

func cleanuptest() {
    fmt.Println("Cleanup.😀")
}
func TestClean(t *testing.T) {
    t.Cleanup(cleanuptest)
    t.Log("testing done.")
}

func init() {
    fmt.Println("hello, i'm \"init\".")
}

func TestMain(m *testing.M) {
    // 在開始運行單元測試代碼之前
    // 可以在此處添加環境初始化相關代碼或者函數調用
    fmt.Println("😀 開始所有測試前")

    retCode := m.Run()

    // 在全部測試代碼運行結束退出之前
    // 可以在此處添加清理代碼或函數調用
    fmt.Println("😀 結束所有測試前")

    os.Exit(retCode)
}

使用 go test -v 運行得到:

~/Projects/go/examples/hellotest 
➜  go test -v
hello, i'm "init".
😀 開始所有測試前
=== RUN   TestHello
    youwu_test.go:10: TestHello
--- PASS: TestHello (0.00s)
=== RUN   TestClean
    youwu_test.go:22: testing done.
Cleanup.😀
--- PASS: TestClean (0.00s)
PASS
😀 結束所有測試前
ok      hellotest       0.369s

以上的順序,可以概括爲如下的流程:

`init()` → `TestMain` → TestXXX
                           ↓  
              ↑       ← t.Cleanup

benckmark | 性能基準測試

go testing 除了函數正確性測試之外,還支持性能基準測試(benchmark),即可多次運行,並計算其平均執行時間從而評估其運行效率。網上一些開源的 go 項目經常會擺出一些與同類工具的性能基準測試對比,就是使用 go testing benchmark 輕鬆做出來的。

性能基準測試的代碼命名規範與普通 testing 代碼的要求類似,除部分性能基準測試專有的函數外,其它像報告打印、跳出機制都一樣。不過由於 benchmark 程序會多次執行,除非有必要,在測試函數中儘量少用打印函數。

go 性能基準測試代碼的函數命名:

func BenchmarkXxx(*testing.T)

這些基準測試代碼可以與普通的單元測試代碼放在一起。通常使用 go test 不會進行基準測試,需要添加參數。

還是 youwu_test.go

// youwu_test.go
package main_test

import (
    "fmt"
    "math/rand"
    "os"
    "testing"
)

func TestHello(t *testing.T) {
    t.Log("TestHello")
}

func init() {
    fmt.Println("hello, i'm \"init\".")
}

func TestMain(m *testing.M) {
    // 在開始運行單元測試代碼之前
    // 可以在此處添加環境初始化相關代碼或者函數調用
    fmt.Println("😀 開始所有測試前")

    retCode := m.Run()

    // 在全部測試代碼運行結束退出之前
    // 可以在此處添加清理代碼或函數調用
    fmt.Println("😀 結束所有測試前")

    os.Exit(retCode)
}

func BenchmarkRandInt(b *testing.B) {
    for i := 0; i < b.N; i++ {
        rand.Int()
    }
}

使用命令 go test -bench='Rand' -v .得到的結果如下:

~/Projects/go/examples/hellotest 
➜  go test -bench='Rand' -v .
hello, i'm "init".
😀 開始所有測試前
=== RUN   TestHello
    youwu_test.go:19: TestHello
--- PASS: TestHello (0.00s)
goos: darwin
goarch: amd64
pkg: hellotest
cpu: Intel(R) Core(TM) i5-7500 CPU @ 3.40GHz
BenchmarkRandInt
BenchmarkRandInt-4      70186930                16.21 ns/op
PASS
😀 結束所有測試前
ok      hellotest       1.476s

可見,普通的單元測試代碼還是會被執行。使用 -bench='Rand' 參數來指定 go testing 執行 BenchmarkRandInt 這個函數。終端報告中的

BenchmarkRandInt-4      70186930                16.21 ns/op

表示運行了 70186930 次,每次 16.21 納秒。更多支持請看官方文檔,或者命令 go help test 查看關於 benchmark 部分。

轉自:

youwu.today/skill/backend/how-to-test-your-go-code

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/24P5VdZNt5Z5mTWu7GmXPA