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
組織你的單元測試代碼
-
測試代碼文件名
xxx_test.go
,必須遵守。 -
測試函數簽名
TestXXX(t *testing.T)
,必須遵守。 -
測試文件所在包,不強制。可以與對應的功能代碼文件位於同一個包,開發者根據自己的需要來確定工程規範
爲了更好的管理單元測試代碼,按照 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
函數即可。
- 初始化 init 方式
任何 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
- 清理 有兩種級別的清理,一種是對於當前單元測試函數(這非常類似於 defer 的延後執行),一種是包級別的。
測試函數級別:使用 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 方式
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
組織你的單元測試代碼
-
測試代碼文件名
xxx_test.go
,必須遵守。 -
測試函數簽名
TestXXX(t *testing.T)
,必須遵守。 -
測試文件所在包,不強制。可以與對應的功能代碼文件位於同一個包,開發者根據自己的需要來確定工程規範
爲了更好的管理單元測試代碼,按照 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
函數即可。
- 初始化 init 方式
任何 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
- 清理 有兩種級別的清理,一種是對於當前單元測試函數(這非常類似於 defer 的延後執行),一種是包級別的。
測試函數級別:使用 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 方式
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