一、單元測試
要開始一個單元測試,需要準備一個 go 源碼檔案,在命名檔案時需要讓檔案必須以
_test
結尾。
單元測試源碼檔案可以由多個測試用例組成,每個測試用例函數需要以
Test
為字首,例如:
func TestXXX( t *testing.T )
- 測試用例檔案不會參與正常源碼編譯,不會被包含到可執行檔案中。
- 測試用例檔案使用 go test 指令來執行,沒有也不需要 main() 作為函數入口。所有在以
結尾的源碼内以_test
開頭的函數會自動被執行。Test
- 測試用例可以不傳入 *testing.T 參數。
helloworld 的測試代碼:
package code11_3
import "testing"
func TestHelloWorld(t *testing.T) {
t.Log("hello world")
}
代碼說明如下:
- 第 5 行,單元測試檔案 (*_test.go) 裡的測試入口必須以 Test 開始,參數為 *testing.T 的函數。一個單元測試檔案可以有多個測試入口。
- 第 6 行,使用 testing 包的 T 結構提供的 Log() 方法列印字元串。
1) 單元測試指令行
單元測試使用 go test 指令啟動,例如:
$ go test helloworld_test.go
ok command-line-arguments 0.003s
$ go test -v helloworld_test.go
=== RUN TestHelloWorld
--- PASS: TestHelloWorld (0.00s)
helloworld_test.go:8: hello world
PASS
ok command-line-arguments 0.004s
代碼說明如下:
- 第 1 行,在 go test 後跟 helloworld_test.go 檔案,表示測試這個檔案裡的所有測試用例。
- 第 2 行,顯示測試結果,ok 表示測試通過,command-line-arguments 是測試用例需要用到的一個包名,0.003s 表示測試花費的時間。
- 第 3 行,顯示在附加參數中添加了
,可以讓測試時顯示詳細的流程。-v
- 第 4 行,表示開始運作名叫 TestHelloWorld 的測試用例。
- 第 5 行,表示已經運作完 TestHelloWorld 的測試用例,PASS 表示測試成功。
- 第 6 行列印字元串 hello world。
2) 運作指定單元測試用例
go test 指定檔案時預設執行檔案内的所有測試用例。可以使用
-run
參數選擇需要的測試用例單獨執行,參考下面的代碼。
一個檔案包含多個測試用例(具體位置是
./src/chapter11/gotest/select_test.go
)
package code11_3
import "testing"
func TestA(t *testing.T) {
t.Log("A")
}
func TestAK(t *testing.T) {
t.Log("AK")
}
func TestB(t *testing.T) {
t.Log("B")
}
func TestC(t *testing.T) {
t.Log("C")
}
這裡指定 TestA 進行測試:
$ go test -v -run TestA select_test.go
=== RUN TestA
--- PASS: TestA (0.00s)
select_test.go:6: A
=== RUN TestAK
--- PASS: TestAK (0.00s)
select_test.go:10: AK
PASS
ok command-line-arguments 0.003s
TestA 和 TestAK 的測試用例都被執行,原因是
-run
跟随的測試用例的名稱支援正規表達式,使用
-run TestA$
即可隻執行 TestA 測試用例。
3) 标記單元測試結果
當需要終止目前測試用例時,可以使用 FailNow,參考下面的代碼。
測試結果标記(具體位置是
./src/chapter11/gotest/fail_test.go
)
func TestFailNow(t *testing.T) {
t.FailNow()
}
還有一種隻标記錯誤不終止測試的方法,代碼如下:
func TestFail(t *testing.T) {
fmt.Println("before fail")
t.Fail()
fmt.Println("after fail")
}
測試結果如下:
=== RUN TestFail
before fail
after fail
--- FAIL: TestFail (0.00s)
FAIL
exit status 1
FAIL command-line-arguments 0.002s
從日志中看出,第 5 行調用 Fail() 後測試結果标記為失敗,但是第 7 行依然被程式執行了。
4) 單元測試日志
每個測試用例可能并發執行,使用 testing.T 提供的日志輸出可以保證日志跟随這個測試上下文一起列印輸出。testing.T 提供了幾種日志輸出方法,詳見下表所示。
方 法 | 備 注 |
---|---|
Log | 列印日志,同時結束測試 |
Logf | 格式化列印日志,同時結束測試 |
Error | 列印錯誤日志,同時結束測試 |
Errorf | 格式化列印錯誤日志,同時結束測試 |
Fatal | 列印緻命日志,同時結束測試 |
Fatalf | 格式化列印緻命日志,同時結束測試 |
開發者可以根據實際需要選擇合适的日志。
二、基準測試
基準測試可以測試一段程式的運作性能及耗費 CPU 的程度。Go 語言中提供了基準測試架構,使用方法類似于單元測試,使用者無須準備高精度的計時器和各種分析工具,基準測試本身即可以列印出非常标準的測試報告。
1) 基礎測試基本使用
下面通過一個例子來了解基準測試的基本使用方法。
基準測試(具體位置是
./src/chapter11/gotest/benchmark_test.go
)
package code11_3
import "testing"
func Benchmark_Add(b *testing.B) {
var n int
for i := 0; i < b.N; i++ {
n++
}
}
這段代碼使用基準測試架構測試加法性能。第 7 行中的 b.N 由基準測試架構提供。測試代碼需要保證函數可重入性及無狀态,也就是說,測試代碼不使用全局變量等帶有記憶性質的資料結構。避免多次運作同一段代碼時的環境不一緻,不能假設 N 值範圍。
使用如下指令行開啟基準測試:
$ go test -v -bench=. benchmark_test.go
goos: linux
goarch: amd64
Benchmark_Add-4 20000000 0.33 ns/op
PASS
ok command-line-arguments 0.700s
代碼說明如下:
- 第 1 行的
表示運作 benchmark_test.go 檔案裡的所有基準測試,和單元測試中的-bench=.
類似。-run
- 第 4 行中顯示基準測試名稱,2000000000 表示測試的次數,也就是 testing.B 結構中提供給程式使用的 N。“0.33 ns/op”表示每一個操作耗費多少時間(納秒)。
注意:Windows 下使用 go test 指令行時,
-bench=.
應寫為
-bench="."
。
2) 基準測試原理
基準測試架構對一個測試用例的預設測試時間是 1 秒。開始測試時,當以 Benchmark 開頭的基準測試用例函數傳回時還不到 1 秒,那麼 testing.B 中的 N 值将按 1、2、5、10、20、50……遞增,同時以遞增後的值重新調用基準測試用例函數。
3) 自定義測試時間
通過
-benchtime
參數可以自定義測試時間,例如:
$ go test -v -bench=. -benchtime=5s benchmark_test.go
goos: linux
goarch: amd64
Benchmark_Add-4 10000000000 0.33 ns/op
PASS
ok command-line-arguments 3.380s
4) 測試記憶體
基準測試可以對一段代碼可能存在的記憶體配置設定進行統計,下面是一段使用字元串格式化的函數,内部會進行一些配置設定操作。
func Benchmark_Alloc(b *testing.B) {
for i := 0; i < b.N; i++ {
fmt.Sprintf("%d", i)
}
}
在指令行中添加
-benchmem
參數以顯示記憶體配置設定情況,參見下面的指令:
$ go test -v -bench=Alloc -benchmem benchmark_test.go
goos: linux
goarch: amd64
Benchmark_Alloc-4 20000000 109 ns/op 16 B/op 2 allocs/op
PASS
ok command-line-arguments 2.311s
代碼說明如下:
- 第 1 行的代碼中
後添加了 Alloc,指定隻測試 Benchmark_Alloc() 函數。-bench
- 第 4 行代碼的“16 B/op”表示每一次調用需要配置設定 16 個位元組,“2 allocs/op”表示每一次調用有兩次配置設定。
開發者根據這些資訊可以迅速找到可能的配置設定點,進行優化和調整。
5) 控制計時器
有些測試需要一定的啟動和初始化時間,如果從 Benchmark() 函數開始計時會很大程度上影響測試結果的精準性。testing.B 提供了一系列的方法可以友善地控制計時器,進而讓計時器隻在需要的區間進行測試。我們通過下面的代碼來了解計時器的控制。
基準測試中的計時器控制(具體位置是
./src/chapter11/gotest/benchmark_test.go
):
func Benchmark_Add_TimerControl(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() 可以重置計數器的資料。
計數器内部不僅包含耗時資料,還包括記憶體配置設定的資料。
package chapter10
import (
"fmt"
"strconv"
"testing"
)
// BenchmarkSprintf 對 fmt.Sprintf 函數進行基準測試
func BenchmarkSprintf(b *testing.B) {
number := 10
b.ResetTimer()
for i := 0; i < b.N; i++ {
fmt.Sprintf("%d", number)
}
}
// BenchmarkFormat 對 strconv.FormatInt 函數進行基準測試
func BenchmarkFormat(b *testing.B) {
number := int64(10)
b.ResetTimer()
for i := 0; i < b.N; i++ {
strconv.FormatInt(number, 10)
}
}
// BenchmarkItoa 對 strconv.Itoa 函數進行基準測試
func BenchmarkItoa(b *testing.B) {
number := 10
b.ResetTimer()
for i := 0; i < b.N; i++ {
strconv.Itoa(number)
}
}
三、并發基準測試
func BenchmarkCombinationParallel(b *testing.B) {
// 測試一個對象或者函數在多線程的場景下面是否安全
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
m := rand.Intn(100) + 1
n := rand.Intn(m)
combination(m, n)
}
})
}
RunParallel并發的執行benchmark。RunParallel建立多個goroutine然後把b.N個疊代測試分布到這些goroutine上。goroutine的數目預設是GOMAXPROCS。如果要增加non-CPU-bound的benchmark的并個數,在執行RunParallel之前調用SetParallelism。