Go語言擁有一套單元測試和性能測試系統,僅需要添加很少的代碼就可以快速測試一段需求代碼。
性能測試系統可以給出代碼的性能數據,幫助測試者分析性能問題。
單元測試
概述
單元測試(unit testing),是指對軟件中的最小可測試單元進行檢查和驗證。對於單元測試中單元的含義,一般要根據實際情況去判定其具體含義,如C語言中單元指一個函數,Java里單元指一個類,圖形化的軟件中可以指一個窗口或一個菜單等。總的來說,單元就是人為規定的最小的被測功能模塊。
單元測試是在軟件開發過程中要進行的最低級別的測試活動,軟件的獨立單元將在與程序的其他部分相隔離的情況下進行測試。
testing 提供對 Go 包的自動化測試的支持。通過 go test
命令,能夠自動執行如下形式的任何函數:
func TestXxx(*testing.T)
- 測試用例文件不會參與正常源碼編譯,不會被包含到可執行文件中。
- 測試用例文件使用 go test 指令來執行,沒有也不需要 main() 作為函數入口。所有在以
_test
結尾的源碼內以Test
開頭的函數會自動被執行。 - 測試用例可以不傳入 *testing.T 參數。
在這些函數中,使用 Error, Fail 或相關方法來發出失敗信號。
要編寫一個新的測試套件,需要創建一個名稱以 _test.go 結尾的文件,該文件包含 TestXxx
函數,如上所述。 將該文件放在與被測試的包相同的包中。該文件將被排除在正常的程序包之外,但在運行 “go test” 命令時將被包含。 有關詳細信息,請運行 “go help test” 和 “go help testflag” 了解。
如果有需要,可以調用 *T 和 *B 的 Skip 方法,跳過該測試或基准測試:
func TestTimeConsuming(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
...
}
Go語言的單元測試對文件名和方法名,參數都有很嚴格的要求。
- 文件名必須以xxx_test.go命名
- 方法必須是Test[^a-z]開頭
- *方法參加必須 t testing.T
- 使用go test執行單元測試
go test參數解讀
go test是go語言自帶的測試工具,其中包含的是兩類,單元測試和性能測試
通過go help test可以看到go test的使用說明:
格式形如:
go test [-c] [-i] [build/test flags] [packages] [build/test flags & test binary flags]
參數解讀:
-c : 編譯go test成為可執行的二進制文件,但是不運行測試。
-i : 安裝測試包依賴的package,但是不運行測試。
關於build flags,調用go help build,這些是編譯運行過程中需要使用到的參數,一般設置為空
關於packages,調用go help packages,這些是關於包的管理,一般設置為空
關於flags for test binary,調用go help testflag,這些是go test過程中經常使用到的參數
-test.v : 是否輸出全部的單元測試用例(不管成功或者失敗),默認沒有加上,所以只輸出失敗的單元測試用例。
-test.run pattern: 只跑哪些單元測試用例
-test.bench patten: 只跑那些性能測試用例
-test.benchmem : 是否在性能測試的時候輸出內存情況
-test.benchtime t : 性能測試運行的時間,默認是1s
-test.cpuprofile cpu.out : 是否輸出cpu性能分析文件
-test.memprofile mem.out : 是否輸出內存性能分析文件
-test.blockprofile block.out : 是否輸出內部goroutine阻塞的性能分析文件
-test.memprofilerate n : 內存性能分析的時候有一個分配了多少的時候才打點記錄的問題。這個參數就是設置打點的內存分配間隔,也就是profile中一個sample代表的內存大小。默認是設置為512 * 1024的。如果你將它設置為1,則每分配一個內存塊就會在profile中有個打點,那么生成的profile的sample就會非常多。如果你設置為0,那就是不做打點了。
你可以通過設置memprofilerate=1和GOGC=off來關閉內存回收,並且對每個內存塊的分配進行觀察。
-test.blockprofilerate n: 基本同上,控制的是goroutine阻塞時候打點的納秒數。默認不設置就相當於-test.blockprofilerate=1,每一納秒都打點記錄一下
-test.parallel n : 性能測試的程序並行cpu數,默認等於GOMAXPROCS。
-test.timeout t : 如果測試用例運行時間超過t,則拋出panic
-test.cpu 1,2,4 : 程序運行在哪些CPU上面,使用二進制的1所在位代表,和nginx的nginx_worker_cpu_affinity是一個道理
-test.short : 將那些運行時間較長的測試用例運行時間縮短
示例:
定義一個test包,包內有一個加法和減法的函數
package test
func Sum(a int, b int) int {
return a + b
}
func Sub(a int, b int) int {
return a - b
}
測試文件的名稱不需要和包文件名稱一樣,也可以叫abc_test.go等
測試文件test_test.go的測試代碼如下
package test
import (
"testing"
)
//編寫一個測試用例,去測試Sum函數是否正確
func TestSum(t *testing.T) {
res := Sum(10, 20)
if res != 30 {
t.Fatalf("Sum(10, 20)執行錯誤")
}
//如果正確,輸出日志
t.Logf("Sum(10, 20)執行正確")
}
func TestOk(t *testing.T) {
t.Logf("這個方法也進來啦")
}
func TestSub(t *testing.T) {
res := Sub(10, 5)
if res != 5 {
t.Fatalf("Sub(10, 5)執行錯誤")
}
//如果正確,輸出日志
t.Logf("Sub(10, 5)執行正確")
}
在終端進入該包的目錄內,使用go test命令進行測試
加上-v
參數可以讓測試時顯示詳細的流程。
我們再新建一個abc_test.go文件,把test_test.go文件里的TestSub()方法挪到abc_test.go文件中
然后重新執行go test -v命令
可以看到test_test.go內的方法都被執行了,由此可知,使用go test -v命令會把該包目錄內的所有測試文件的所有方法都執行一遍。
如果想測試包里面的單個文件,一定要帶上被測試的原文件,如
go test -v abc_test.go test.go
如果想測試單個方法,需要加上 -run
參數,並且方法名末尾要加上$
,原因是-run
跟隨的測試用例的名稱支持正則表達式,使用-run TestSub$
即可只執行 TestSub 測試用例。否則會執行所有以TestSub開頭的所有函數如,修改abc_test.go
package test
import (
"testing"
)
//編寫一個測試用例,去測試Sum函數是否正確
func TestSum(t *testing.T) {
res := Sum(10, 20)
if res != 30 {
t.Fatalf("Sum(10, 20)執行錯誤")
}
//如果正確,輸出日志
t.Logf("Sum(10, 20)執行正確")
}
func TestOk(t *testing.T) {
t.Logf("這個方法也進來啦")
}
func TestSub2(t *testing.T) {
t.Logf("進入到TestSub2方法里來了")
}
go test -v -test.run TestSub$
或
go test -v -run TestSub$
如果不加$
,所有以TestSub開頭的所有測試函數都會被執行
單元測試日志
每個測試用例可能並發執行,使用 testing.T 提供的日志輸出可以保證日志跟隨這個測試上下文一起打印輸出。testing.T 提供了幾種日志輸出方法,詳見下表所示。
方 法 | 備 注 |
---|---|
Log | 打印日志,同時結束測試 |
Logf | 格式化打印日志,同時結束測試 |
Error | 打印錯誤日志,同時結束測試 |
Errorf | 格式化打印錯誤日志,同時結束測試 |
Fatal | 打印致命日志,同時結束測試 |
Fatalf | 格式化打印致命日志,同時結束測試 |
基准測試
基准測試可以測試一段程序的運行性能及耗費 CPU 的程度。Go 語言中提供了基准測試框架,使用方法類似於單元測試,使用者無須准備高精度的計時器和各種分析工具,基准測試本身即可以打印出非常標准的測試報告。
壓力測試用來檢測函數(方法)的性能,和編寫單元功能測試的方法類似,但需要注意以下幾點:
-
壓力測試用例必須遵循如下格式,其中XXX可以是任意字母數字的組合,但是首字母不能是小寫字母
func BenchmarkXXX(b *testing.B) { ... }
-
go test不會默認執行壓力測試的函數,如果要執行壓力測試需要帶上參數-test.bench,語法:-test.bench="test_name_regex",例如go test -test.bench=".*"表示測試全部的壓力測試函數
-
在壓力測試用例中,請記得在循環體內使用testing.B.N,以使測試可以正常的運行
-
文件名也必須以_test.go結尾
基礎測試基本使用
新建一個壓力測試文件bench_test.go
目錄結構
bench_test.go代碼
package test
import "testing"
func BenchmarkSub(b *testing.B) {
for i := 0; i < b.N; i++ { //use b.N for looping
Sub(10, 5)
}
}
func BenchmarkTimeConsumingFunction(b *testing.B) {
b.StopTimer() //調用該函數停止壓力測試的時間計數
//做一些初始化的工作,例如讀取文件數據,數據庫連接之類的,
//這樣這些時間不影響我們測試函數本身的性能
b.StartTimer() //重新開始時間
for i := 0; i < b.N; i++ {
Sub(10, 5)
}
}
這段代碼使用基准測試框架測試減法性能。第 7 行中的 b.N 由基准測試框架提供。測試代碼需要保證函數可重入性及無狀態,也就是說,測試代碼不使用全局變量等帶有記憶性質的數據結構。避免多次運行同一段代碼時的環境不一致,不能假設 N 值范圍。
執行命令
go test -test.bench=".*"
輸出結果:
goos: darwin
goarch: amd64
pkg: test
BenchmarkSub-8 2000000000 0.32 ns/op
//BenchmarkSub執行了2000000000次,每次的執行平均時間是0.32納秒
BenchmarkTimeConsumingFunction-8 2000000000 0.31 ns/op
//BenchmarkTimeConsumingFunction,執行了2000000000次,每次的執行平均時間是0.31納秒
PASS
ok test 1.328s
我們還可以使用-count
執行次數
go test -test.bench=".*" -count=5
輸出結果:
goos: darwin
goarch: amd64
pkg: test
BenchmarkSub-8 2000000000 0.31 ns/op
BenchmarkSub-8 2000000000 0.32 ns/op
BenchmarkSub-8 2000000000 0.32 ns/op
BenchmarkSub-8 2000000000 0.31 ns/op
BenchmarkSub-8 2000000000 0.31 ns/op
BenchmarkTimeConsumingFunction-8 2000000000 0.31 ns/op
BenchmarkTimeConsumingFunction-8 2000000000 0.31 ns/op
BenchmarkTimeConsumingFunction-8 2000000000 0.31 ns/op
BenchmarkTimeConsumingFunction-8 2000000000 0.31 ns/op
BenchmarkTimeConsumingFunction-8 2000000000 0.31 ns/op
PASS
ok test 6.573s
基准測試原理
基准測試框架對一個測試用例的默認測試時間是 1 秒。開始測試時,當以 Benchmark 開頭的基准測試用例函數返回時還不到 1 秒,那么 testing.B 中的 N 值將按 1、2、5、10、20、50……遞增,同時以遞增后的值重新調用基准測試用例函數。
自定義測試時間
通過-benchtime
參數可以自定義測試時間,例如:
go test -test.bench=".*" -count=5 -benchtime=5s
測試內存
基准測試可以對一段代碼可能存在的內存分配進行統計,下面是一段使用字符串格式化的函數,內部會進行一些分配操作。
func Benchmark_Alloc(b *testing.B) {
for i := 0; i < b.N; i++ {
fmt.Sprintf("%d", i)
}
}
在命令行中添加-benchmem
參數以顯示內存分配情況,參見下面的指令:
bogon:test itbsl$ go test -test.bench=Alloc -benchmem
goos: darwin
goarch: amd64
pkg: test
BenchmarkAlloc-8 20000000 111 ns/op 16 B/op 2 allocs/op
PASS
ok test 2.354s
代碼說明如下:
- 第 1 行的代碼中
-bench
后添加了 Alloc,指定只測試 Benchmark_Alloc() 函數。 - 第 4 行代碼的“16 B/op”表示每一次調用需要分配 16 個字節,“2 allocs/op”表示每一次調用有兩次分配。
開發者根據這些信息可以迅速找到可能的分配點,進行優化和調整。
控制計時器
有些測試需要一定的啟動和初始化時間,如果從 Benchmark() 函數開始計時會很大程度上影響測試結果的精准性。testing.B 提供了一系列的方法可以方便地控制計時器,從而讓計時器只在需要的區間進行測試。我們通過下面的代碼來了解計時器的控制。
基准測試中的計時器控制
func BenchmarkAddTimerControl(b *testing.B) {
// 重置計時器
b.ResetTimer()
// 停止計時器
b.StopTimer()
// 開始計時器
b.StartTimer()
var n int
for i := 0; i < b.N; i++ {
n++
}
}
從 Benchmark() 函數開始,Timer 就開始計數。StopTimer() 可以停止這個計數過程,做一些耗時的操作,通過 StartTimer() 重新開始計時。ResetTimer() 可以重置計數器的數據。
計數器內部不僅包含耗時數據,還包括內存分配的數據。