1. 程式人生 > 其它 >GoalngTest完全攻略

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語言中提供了基準測試框架,使用方法類似於單元測試,使用者無須準備高精度的計時器和各種分析工具,基準測試本身即可以打印出非常標準的測試報告。

  1. 基準測試的程式碼檔案必須以_test.go結尾
  2. 基準測試的函式必須以Benchmark開頭,必須是可匯出的
  3. 基準測試函式必須接受一個指向Benchmark型別的指標作為唯一引數
  4. 基準測試函式不能有返回值
  5. b.ResetTimer是重置計時器,這樣可以避免for迴圈之前的初始化程式碼的干擾
  6. 最後的for迴圈很重要,被測試的程式碼要放到迴圈裡
  7. 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 中的 -8GOMAXPROCS,預設等於 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 引數可以度量記憶體分配的次數。記憶體分配次數也效能也是息息相關的,例如不合理的切片容量,將導致記憶體重新分配,帶來不必要的開銷。

在下面的例子中,generateWithCapgenerate 的作用是一致的,生成一組長度為 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 開始計時。

例如,如果測試一個冒泡函式的效能,每次呼叫冒泡函式前,需要隨機生成一個數字序列,這是非常耗時的操作,這種場景下,就需要使用 StopTimerStartTimer 避免將這部分時間計算在內。

例如:

// 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

附 推薦與參考