Go語言單元測試與基准測試


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語言的單元測試對文件名和方法名,參數都有很嚴格的要求。

  1. 文件名必須以xxx_test.go命名
  2. 方法必須是Test[^a-z]開頭
  3. *方法參加必須 t testing.T
  4. 使用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 語言中提供了基准測試框架,使用方法類似於單元測試,使用者無須准備高精度的計時器和各種分析工具,基准測試本身即可以打印出非常標准的測試報告。

壓力測試用來檢測函數(方法)的性能,和編寫單元功能測試的方法類似,但需要注意以下幾點:

  1. 壓力測試用例必須遵循如下格式,其中XXX可以是任意字母數字的組合,但是首字母不能是小寫字母

    func BenchmarkXXX(b *testing.B) { ... }
    
  2. go test不會默認執行壓力測試的函數,如果要執行壓力測試需要帶上參數-test.bench,語法:-test.bench="test_name_regex",例如go test -test.bench=".*"表示測試全部的壓力測試函數

  3. 在壓力測試用例中,請記得在循環體內使用testing.B.N,以使測試可以正常的運行

  4. 文件名也必須以_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() 可以重置計數器的數據。

計數器內部不僅包含耗時數據,還包括內存分配的數據。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM