GoalngTest完全攻略
一、提示
go test 命令,會自動讀取原始碼目錄下面名為 *_test.go 的檔案,生成並執行測試用的可執行檔案。
輸出的資訊類似下面所示的樣子:
➜ 3.go測試的操作 go test -v
=== RUN Test_str
go_test.go:9: 測試1
--- PASS: Test_str (0.00s)
PASS
ok MyTest 0.432s
效能測試系統可以給出程式碼的效能資料,幫助測試者分析效能問題。
單元測試(unit testing),是指對軟體中的最小可測試單元進行檢查和驗證。對於單元測試中單元的含義,一般要根據實際情況去判定其具體含義,如C語言中單元指一個函式,Java 裡單元指一個類,圖形化的軟體中可以指一個視窗或一個選單等。總的來說,單元就是人為規定的最小的被測功能模組。
單元測試是在軟體開發過程中要進行的最低級別的測試活動,軟體的獨立單元將在與程式的其他部分相隔離的情況下進行測試。
二、單元測試
要開始一個單元測試,需要準備一個 go 原始碼檔案,在命名檔案時需要讓檔案必須以_test
結尾。預設的情況下,go test
命令不需要任何的引數,它會自動把你原始碼包下面所有 test 檔案測試完畢,當然你也可以帶上引數。
這裡介紹幾個常用的引數:
- -bench regexp 執行相應的 benchmarks,例如 -bench=.;
- -cover 開啟測試覆蓋率;
- -run regexp 只執行 regexp 匹配的函式,例如 -run=Array 那麼就執行包含有 Array 開頭的函式;
- -v 顯示測試的詳細命令。
單元測試原始碼檔案可以由多個測試用例組成,每個測試用例函式需要以Test
為字首,例如:
func Test_str(t *testing.T)
- 測試用例檔案不會參與正常原始碼編譯,不會被包含到可執行檔案中。
- 測試用例檔案使用
go test
指令來執行,沒有也不需要 main() 作為函式入口。所有在以_test
結尾的原始碼內以Test
開頭的函式會自動被執行。 - 測試用例可以不傳入 *testing.T 引數。
測試程式碼示例:
package MyTest
import (
"testing"
)
func Test_str(t *testing.T) { // 單元測試函式必須以 Test 開始,引數為 *testing.T 的函式。
t.Log("測試1") // 使用 testing 包的 T 結構提供的 Log() 方法列印字串
}
2.1、單元測試命令列
單元測試使用 go test 命令啟動,例如:
➜ 3.go測試的操作 git:(main) go test unit_test.go //
ok command-line-arguments 0.447s
第一行:在 go test 後跟 go_test.go 檔案,表示測試這個檔案裡的所有測試用例
第二行:顯示測試結果,ok 表示測試通過,command-line-arguments 是測試用例需要用到的一個包名,0.003s 表示測試花費的時間
➜ 3.go測試的操作 git:(main) go test -v unit_test.go
=== RUN Test_str
go_test.go:9: 測試1
--- PASS: Test_str (0.00s)
PASS
ok command-line-arguments 0.081s
第一行:顯示在附加引數中添加了-v
,可以讓測試時顯示詳細的流程
第二行:表示開始執行名叫 Test_str 的測試用例
第三行:是測試程式碼的輸出
第四行:表示已經執行完 Test_str 的測試用例,PASS 表示測試成功
2.2、執行指定單元測試用例
go test
指定檔案時預設執行檔案內的所有測試用例。可以使用-run
引數選擇需要的測試用例單獨執行,參考下面的程式碼。
package MyTest
import (
"testing"
)
func Test_A(t *testing.T) {
t.Log("測試A")
}
func Test_B(t *testing.T) {
t.Log("測試B")
}
這裡指定 Test_A 進行測試:
➜ 3.go測試的操作 git:(main) ✗ go test -v unit_test.go -run Test_A
=== RUN Test_A
go_test.go:9: 測試A
--- PASS: Test_A (0.00s)
=== RUN Test_A1
go_test.go:13: 測試A1
--- PASS: Test_A1 (0.00s)
PASS
ok command-line-arguments 0.428s
結果令人意外,雖然在命令中指定了只執行Test_A
,但是結果卻執了測試A
, 測試A1
, 這是因為 -run
支援正則表示式,所以自動匹配走了有相同字首的單元測試。可以使用-run TestA$
來精確指定
➜ 3.go測試的操作 git:(main) ✗ go test -v -run Test_A$
=== RUN Test_A
go_test.go:9: 測試A
--- PASS: Test_A (0.00s)
PASS
ok MyTest 0.579s
2.3、標記單元測試結果
當需要終止當前測試用例時,可以使用 FailNow,參考下面的程式碼
// 測試結果標記:需要終止當前測試用例
func TestFailNow(t *testing.T) {
t.Log("TestFailNow before fail") // 會執行
t.FailNow() // 標記為失敗
t.Log("TestFailNow after fail") // 不會執行
}
還有一種只標記錯誤不終止測試的方法,程式碼如下:
// 測試結果標記:只標記錯誤不終止測試
func TestFail(t *testing.T) {
t.Log("TestFail before fail") // 會執行
t.Fail() // 標記為失敗
t.Log("TestFail after fail") // 會執行
}
2.4、單元測試日誌
每個測試用例可能併發執行,使用 testing.T 提供的日誌輸出可以保證日誌跟隨這個測試上下文一起列印輸出。testing.T 提供了幾種日誌輸出方法,詳見下表所示。
方 法 | 備 注 |
---|---|
Log | 列印日誌,同時結束測試 |
Logf | 格式化列印日誌,同時結束測試 |
Error | 列印錯誤日誌,同時結束測試 |
Errorf | 格式化列印錯誤日誌,同時結束測試 |
Fatal | 列印致命日誌,同時結束測試 |
Fatalf | 格式化列印致命日誌,同時結束測試 |
可以根據實際需要選擇合適的日誌。
三、基準測試
基準測試可以測試一段程式的執行效能及耗費 CPU 的程度。Go語言中提供了基準測試框架,使用方法類似於單元測試,使用者無須準備高精度的計時器和各種分析工具,基準測試本身即可以打印出非常標準的測試報告。
- 基準測試的程式碼檔案必須以_test.go結尾
- 基準測試的函式必須以Benchmark開頭,必須是可匯出的
- 基準測試函式必須接受一個指向Benchmark型別的指標作為唯一引數
- 基準測試函式不能有返回值
- b.ResetTimer是重置計時器,這樣可以避免for迴圈之前的初始化程式碼的干擾
- 最後的for迴圈很重要,被測試的程式碼要放到迴圈裡
- b.N是基準測試框架提供的,表示迴圈的次數,因為需要反覆呼叫測試的程式碼,才可以評估效能
3.1、基礎測試基本使用
通過一個例子來了解基準測試的基本使用方法
使用 go mod init example
初始化一個模組,新增 fib.go
檔案,實現函式 fib
,用於計算第 N 個菲波那切數。
func Benchmark_Add(b *testing.B) {
var n int
for i := 0; i < b.N; i++ {
n++
}
}
接下來,在 fib_test.go
中實現一個 benchmark 用例:
package main
import "testing"
func BenchmarkFib(b *testing.B) {
for n := 0; n < b.N; n++ {
fib(30) // run fib(30) b.N times
}
}
程式碼說明如下:
- benchmark 和普通的單元測試用例一樣,都位於
_test.go
檔案中。 - 函式名以
Benchmark
開頭,引數是b *testing.B
。和普通的單元測試用例很像,單元測試函式名以Test
開頭,引數是t *testing.T
。
go test
命令預設不執行 benchmark 用例的,如果想執行 benchmark 用例,則需要加上 -bench
引數。
例如:
➜ benchmark_test git:(main) ✗ go test -v -bench=.
goos: darwin
goarch: arm64
pkg: example
BenchmarkFib
BenchmarkFib-8 318 3790867 ns/op
PASS
ok example 2.734s
-bench
引數支援傳入一個正則表示式,匹配到的用例才會得到執行
例如: 只執行以 Fib
結尾的
➜ benchmark_test git:(main) ✗ go test -v -bench='Fib$' .
goos: darwin
goarch: arm64
pkg: example
BenchmarkFib
BenchmarkFib-8 315 3812223 ns/op
PASS
ok example 1.917s
注意:Windows 下使用 go test 命令列時,-bench=.
應寫為-bench="."
3.2、基準測試的原理
benchmark 用例的引數 b *testing.B
,有個屬性 b.N
表示這個用例需要執行的次數。b.N
對於每個用例都是不一樣的。
那這個值是如何決定的呢?b.N
從 1 開始,如果該用例能夠在 1s 內完成,b.N
的值便會增加,再次執行。b.N
的值大概以 1, 2, 3, 5, 10, 20, 30, 50, 100 這樣的序列遞增,越到後面,增加得越快。
舉個例子,更加容易理解基準測試的原理:
這個例子中每呼叫一次就會列印一次 b.N的值 [注意:-benchtime 用於設定測試時間,下面有講述其用法]
func BenchmarkFff(b *testing.B) {
for n := 0; n < b.N; n++ {
// 停頓1秒
time.Sleep(1 * time.Second)
}
b.Log("呼叫了一次", b.N)
}
執行結果:
➜ benchmark_test git:(main) ✗ go test -v -bench='Fff$' . -benchtime=10s .
goos: darwin
goarch: arm64
pkg: example
BenchmarkFff
fib_test.go:26: 呼叫了一次 1
fib_test.go:26: 呼叫了一次 10
BenchmarkFff-8 10 1000674900 ns/op
PASS
ok example 12.130s
從結果來看,BenchmarkFff 一共被呼叫了兩次,第一次b.N
的值為 1, 第二次為10,這個例子充分的說明了Benchmark的執行原理。b.N的值從1開始,如果BenchmarkFff函式可以在10苗內執行完成,那麼b.N的值就會遞增,這個例子中b.N遞增為10,然後繼續執行BenchmarkFff函式,發現BenchmarkFff函式的執行時間超出了限定的10秒,那麼測試就結束了。
再次執行,時間調整為100秒:
➜ benchmark_test git:(main) ✗ go test -v -bench='Fff$' . -benchtime=100s .
goos: darwin
goarch: arm64
pkg: example
BenchmarkFff
fib_test.go:26: 呼叫了一次 1
fib_test.go:26: 呼叫了一次 100
BenchmarkFff-8 100 1000862939 ns/op
PASS
ok example 101.285s
3.3、自定義CPU核數
Benchmark_Add-8 中的 -8
即 GOMAXPROCS
,預設等於 CPU 核數。可以通過 -cpu
引數改變 GOMAXPROCS
,-cpu
支援傳入一個列表作為引數,例如:
➜ benchmark_test git:(main) ✗ go test -bench='Fib$' -cpu=2,4 .
goos: darwin
goarch: arm64
pkg: example
BenchmarkFib-2 316 3798750 ns/op
BenchmarkFib-4 312 3818075 ns/op
PASS
ok example 3.642s
在這個例子中,改變 CPU 的核數對結果幾乎沒有影響,因為這個 Benchmark_Add 的呼叫是序列的。
使用RunParallel進行並行測試,新增一個並行的benchmark:
func BenchmarkFoo(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
fib(30) // run fib(30) b.N times
}
})
}
並行測試一下:
➜ benchmark_test git:(main) ✗ go test -bench='Foo$' -cpu=2,4 .
goos: darwin
goarch: arm64
pkg: example
BenchmarkFoo-2 624 1944998 ns/op
BenchmarkFoo-4 1206 1002599 ns/op
PASS
ok example 3.215s
可以看到多執行緒的效能差異。
3.4、提升測試準確度
3.4.1、自定義測試時間
對於效能測試來說,提升測試準確度的一個重要手段就是增加測試的次數。我們可以使用 -benchtime
和 -count
兩個引數達到這個目的
benchmark 的預設時間是 1s,那麼可以使用 -benchtime
指定為 5s。例如:
➜ benchmark_test git:(main) ✗ go test -bench='Fib$' -benchtime=5s .
goos: darwin
goarch: arm64
pkg: example
BenchmarkFib-8 1561 3774486 ns/op
PASS
ok example 6.700s
這裡發現實際執行的時間是 6.700s,比 benchtime 的 5s 要長,因為測試用例編譯、執行、銷燬等是需要時間的。
將 -benchtime
設定為 5s,用例執行次數也變成了原來的 5倍,每次函式呼叫時間仍為 0.6s,幾乎沒有變化。
3.4.2、自定義測試次數
-benchtime
的值除了是時間外,還可以是具體的次數。例如,執行 30 次可以用 -benchtime=30x
:
➜ benchmark_test git:(main) ✗ go test -bench='Fib$' -benchtime=50x .
goos: darwin
goarch: arm64
pkg: example
BenchmarkFib-8 50 3813326 ns/op
--- BENCH: BenchmarkFib-8
fib_test.go:15: 呼叫了一次 1
fib_test.go:16: 總共執行了 1
fib_test.go:15: 呼叫了一次 50
fib_test.go:16: 總共執行了 51
PASS
ok example 0.264s
呼叫 50 次 fib(30)
,僅花費了 0.264s。
3.4.3、自定義測試輪數
-count
引數可以用來設定 benchmark 的輪數。例如,進行 3 輪 benchmark。
➜ benchmark_test git:(main) ✗ go test -bench=Fib$ -count=2 .
goos: darwin
goarch: arm64
pkg: example
BenchmarkFib-8 310 3816544 ns/op
--- BENCH: BenchmarkFib-8
fib_test.go:15: 呼叫了一次 1
fib_test.go:16: 總共執行了 1
fib_test.go:15: 呼叫了一次 100
fib_test.go:16: 總共執行了 101
fib_test.go:15: 呼叫了一次 310
fib_test.go:16: 總共執行了 411
BenchmarkFib-8 315 3858069 ns/op
--- BENCH: BenchmarkFib-8
fib_test.go:15: 呼叫了一次 1
fib_test.go:16: 總共執行了 412
fib_test.go:15: 呼叫了一次 100
fib_test.go:16: 總共執行了 512
fib_test.go:15: 呼叫了一次 315
fib_test.go:16: 總共執行了 827
PASS
ok example 3.608s
3.5、測試記憶體分配情況
-benchmem
引數可以度量記憶體分配的次數。記憶體分配次數也效能也是息息相關的,例如不合理的切片容量,將導致記憶體重新分配,帶來不必要的開銷。
在下面的例子中,generateWithCap
和 generate
的作用是一致的,生成一組長度為 n 的隨機序列。唯一的不同在於,generateWithCap
建立切片時,將切片的容量(capacity)設定為 n,這樣切片就會一次性申請 n 個整數所需的記憶體。
// generate_test.go
package main
import (
"math/rand"
"testing"
"time"
)
func generateWithCap(n int) []int {
rand.Seed(time.Now().UnixNano())
nums := make([]int, 0, n)
for i := 0; i < n; i++ {
nums = append(nums, rand.Int())
}
return nums
}
func generate(n int) []int {
rand.Seed(time.Now().UnixNano())
nums := make([]int, 0)
for i := 0; i < n; i++ {
nums = append(nums, rand.Int())
}
return nums
}
func BenchmarkGenerateWithCap(b *testing.B) {
for n := 0; n < b.N; n++ {
generateWithCap(1000000)
}
}
func BenchmarkGenerate(b *testing.B) {
for n := 0; n < b.N; n++ {
generate(1000000)
}
}
執行該用例的結果是:
➜ benchmark_test git:(main) ✗ go test -bench=Generate .
goos: darwin
goarch: arm64
pkg: example
BenchmarkGenerateWithCap-8 76 14514814 ns/op
BenchmarkGenerate-8 61 17938094 ns/op
PASS
ok example 2.635s
可以看到生成 100w 個長度的切片,GenerateWithCap
的耗時比 Generate
少 20%。
可以使用 -benchmem
引數看到記憶體分配的情況:
➜ benchmark_test git:(main) ✗ go test -bench=Generate -benchmem .
goos: darwin
goarch: arm64
pkg: example
BenchmarkGenerateWithCap-8 79 14566255 ns/op 8003700 B/op 1 allocs/op
BenchmarkGenerate-8 64 17547603 ns/op 41678197 B/op 39 allocs/op
PASS
ok example 2.381s
Generate
分配的記憶體是 GenerateWithCap
的 5 倍,設定了切片容量,記憶體只分配一次,而不設定切片容量,記憶體分配了 39 次。
3.6、測試不同的輸入
不同的函式複雜度不同,O(1),O(n),O(n^2) 等,利用 benchmark 驗證複雜度一個簡單的方式,是構造不同的輸入。對上面的 benchmark 稍作改造,便能夠達到目的。
// generate_test.go
package main
import (
"math/rand"
"testing"
"time"
)
func generate(n int) []int {
rand.Seed(time.Now().UnixNano())
nums := make([]int, 0)
for i := 0; i < n; i++ {
nums = append(nums, rand.Int())
}
return nums
}
func benchmarkGenerate(i int, b *testing.B) {
for n := 0; n < b.N; n++ {
generate(i)
}
}
func BenchmarkGenerate1000(b *testing.B) { benchmarkGenerate(1000, b) }
func BenchmarkGenerate10000(b *testing.B) { benchmarkGenerate(10000, b) }
func BenchmarkGenerate100000(b *testing.B) { benchmarkGenerate(100000, b) }
func BenchmarkGenerate1000000(b *testing.B) { benchmarkGenerate(1000000, b) }
這裡,實現了一個輔助函式 benchmarkGenerate
允許傳入引數 i,並構造了 4 個不同輸入的 benchmark 用例。執行結果如下:
➜ benchmark_test git:(main) ✗ go test generate2_test.go -bench=Generate .
goos: darwin
goarch: arm64
BenchmarkGenerate1000-8 43047 28351 ns/op
BenchmarkGenerate10000-8 5773 184571 ns/op
BenchmarkGenerate100000-8 669 1855509 ns/op
BenchmarkGenerate1000000-8 62 17722528 ns/op
PASS
ok command-line-arguments 5.638s
通過測試結果可以發現,輸入變為原來的 10 倍,函式每次呼叫的時長也差不多是原來的 10 倍,這說明覆雜度是線性的。
四、Benchmark 注意事項
3.1 ResetTimer
如果在 benchmark 開始前,需要一些準備工作,如果準備工作比較耗時,則需要將這部分程式碼的耗時忽略掉。比如下面的例子:
func BenchmarkFib(b *testing.B) {
time.Sleep(time.Second * 3) // 模擬耗時準備任務
for n := 0; n < b.N; n++ {
fib(30) // run fib(30) b.N times
}
}
執行結果是:
➜ benchmark_test git:(main) ✗ go test fib_test.go fib.go -bench=Fib$ -benchtime=50x
goos: darwin
goarch: arm64
BenchmarkFib-8 50 64149844 ns/op
--- BENCH: BenchmarkFib-8
fib_test.go:16: 呼叫了一次 1
fib_test.go:17: 總共執行了 1
fib_test.go:16: 呼叫了一次 50
fib_test.go:17: 總共執行了 51
PASS
ok command-line-arguments 6.783s
50次呼叫,每次呼叫約 0.64s,是之前的 0.06s 的 11 倍。究其原因,受到了耗時準備任務的干擾。我們需要用 ResetTimer
遮蔽掉:
➜ benchmark_test git:(main) ✗ go test fib_test.go fib.go -bench=Fib$ -benchtime=50x
goos: darwin
goarch: arm64
BenchmarkFib-8 50 3932914 ns/op
--- BENCH: BenchmarkFib-8
fib_test.go:17: 呼叫了一次 1
fib_test.go:18: 總共執行了 1
fib_test.go:17: 呼叫了一次 50
fib_test.go:18: 總共執行了 51
PASS
ok command-line-arguments 6.736s
執行結果恢復正常,每次呼叫約 0.03s。
3.2、StopTimer & StartTimer
還有一種情況,每次函式呼叫前後需要一些準備工作和清理工作,我們可以使用 StopTimer
暫停計時以及使用 StartTimer
開始計時。
例如,如果測試一個冒泡函式的效能,每次呼叫冒泡函式前,需要隨機生成一個數字序列,這是非常耗時的操作,這種場景下,就需要使用 StopTimer
和 StartTimer
避免將這部分時間計算在內。
例如:
// sort_test.go
package main
import (
"math/rand"
"testing"
"time"
)
// generateWithCap 生成隨機數切片,這裡比較耗時,可以使用StopTimer & StartTimer來消除其干擾
func generateWithCap(n int) []int {
rand.Seed(time.Now().UnixNano())
nums := make([]int, 0, n)
for i := 0; i < n; i++ {
nums = append(nums, rand.Int())
}
return nums
}
// bubbleSort 氣泡排序
func bubbleSort(nums []int) {
for i := 0; i < len(nums); i++ {
for j := 1; j < len(nums)-i; j++ {
if nums[j] < nums[j-1] {
nums[j], nums[j-1] = nums[j-1], nums[j]
}
}
}
}
func BenchmarkBubbleSort(b *testing.B) {
for n := 0; n < b.N; n++ {
b.StopTimer()
nums := generateWithCap(10000)
b.StartTimer()
bubbleSort(nums)
}
}
執行該用例,每次排序耗時約 0.05s。
➜ benchmark_test git:(main) ✗ go test sort_test.go -v -bench=BubbleSort$
goos: darwin
goarch: arm64
BenchmarkBubbleSort
BenchmarkBubbleSort-8 21 55139538 ns/op
PASS
ok command-line-arguments 1.602s