?
每種語言通常都有自己的測試包/模塊,Go語言也不例外。在Go中,我們可以通過testing
包對代碼進行單元測試和性能測試。?
如何測試 Go 代碼?
Go語言有自帶的測試框架testing
,可以用來實現單元測試(T類型)和性能測試(B類型),通過go test
命令來執(zhí)行單元測試和性能測試。
go test 執(zhí)行測試用例時,是以go包為單位進行測試的。執(zhí)行時需要指定包名,比如go test 包名
,如果沒有指定包名,默認會選擇執(zhí)行命令時所在的包。
go test在執(zhí)行時,會遍歷以_test.go
結尾的源碼文件,執(zhí)行其中以Test
、Benchmark
、Example
開頭的測試函數。
為了演示如何編寫測試用例,我預先編寫了4個函數。假設這些函數保存在test目錄下的math.go
文件中,包名為test
,math.go代碼如下:
package test
import (
"fmt"
"math"
"math/rand"
)
// Abs returns the absolute value of x.
func Abs(x float64) float64 {
return math.Abs(x)
}
// Max returns the larger of x or y.
func Max(x, y float64) float64 {
return math.Max(x, y)
}
// Min returns the smaller of x or y.
func Min(x, y float64) float64 {
return math.Min(x, y)
}
// RandInt returns a non-negative pseudo-random int from the default Source.
func RandInt() int {
return rand.Int()
}
?
測試命名規(guī)范
在我們對Go代碼進行測試時,需要編寫測試文件、測試函數、測試變量,它們都需要遵循一定的規(guī)范。?
測試文件的命名規(guī)范
Go的測試文件名必須以_test.go
結尾。例如,如果我們有一個名為person.go
的文件,那它的測試文件必須命名為person_test.go
。?
包的命名規(guī)范
Go的測試可以分為白盒測試和黑盒測試。
- 白盒測試:將測試和生產代碼放在同一個Go包中,這使我們可以同時測試Go包中可導出和不可導出的標識符。當我們編寫的單元測試需要訪問Go包中不可導出的變量、函數和方法時,就需要編寫白盒測試用例。
- 黑盒測試:將測試和生產代碼放在不同的Go包中。這時,我們僅可以測試Go包的可導出標識符。這意味著我們的測試包將無法訪問生產代碼中的任何內部函數、變量或常量。
在白盒測試中,Go的測試包名稱需要跟被測試的包名保持一致,例如:person.go
定義了一個person
包,則person_test.go
的包名也要為person
,這也意味著person.go
和person_test.go
都要在同一個目錄中。
在黑盒測試中,Go的測試包名稱需要跟被測試的包名不同,但仍然可以存放在同一個目錄下。比如,person.go
定義了一個person
包,則person_test.go
的包名需要跟person
不同,通常我們命名為person_test
。
?
函數的命名規(guī)范
測試用例函數必須以Test
、Benchmark
、Example
開頭,例如TestXxx
、BenchmarkXxx
、ExampleXxx
,Xxx
部分為任意字母數字的組合,首字母大寫。?
除此之外,還有一些社區(qū)的約束,這些約束不是強制的,但是遵循這些約束會讓我們的測試函數名更加易懂。例如,我們有以下函數:
package main
type Person struct {
age int64
}
func (p *Person) older(other *Person) bool {
return p.age > other.age
}
?
其實,還有其他更好的命名方法。比如,這種情況下,我們可以將函數命名為TestOlderXxx
,其中Xxx
代表Older
函數的某個場景描述。例如,strings.Compare
函數有如下測試函數:TestCompare
、TestCompareIdenticalString
、TestCompareStrings
。
變量的命名規(guī)范
Go語言和go test沒有對變量的命名做任何約束。?
單元測試用例通常會有一個實際的輸出,在單元測試中,我們會將預期的輸出跟實際的輸出進行對比,來判斷單元測試是否通過。為了清晰地表達函數的實際輸出和預期輸出,可以將這兩類輸出命名為expected/actual
,或者got/want
。例如:
if c.expected != actual {
t.Fatalf("Expected User-Agent '%s' does not match '%s'", c.expected, actual)
}
或者:
if got, want := diags[3].Description().Summary, undeclPlural; got != want {
t.Errorf("wrong summary for diagnostic 3\ngot: %s\nwant: %s", got, want)
}
其他的變量命名,我們可以遵循Go語言推薦的變量命名方法,例如:
- Go中的變量名應該短而不是長,對于范圍有限的局部變量來說尤其如此。
- 變量離聲明越遠,對名稱的描述性要求越高。
- 像循環(huán)、索引之類的變量,名稱可以是單個字母(i)。如果是不常見的變量和全局變量,變量名就需要具有更多的描述性。
?
單元測試
單元測試用例函數以?Test
?開頭,例如?TestXxx
?或?Test_xxx
?(?Xxx
?部分為任意字母數字組合,首字母大寫)。函數參數必須是?*testing.T
,可以使用該類型來記錄錯誤或測試狀態(tài)。
我們可以調用?testing.T
?的?Error
?、Errorf
?、FailNow
?、Fatal
?、FatalIf
?方法,來說明測試不通過;調用?Log
?、Logf
?方法來記錄測試信息。函數列表和相關描述如下表所示:
下面的代碼是兩個簡單的單元測試函數(函數位于文件math_test.go中):
func TestAbs(t *testing.T) {
got := Abs(-1)
if got != 1 {
t.Errorf("Abs(-1) = %f; want 1", got)
}
}
func TestMax(t *testing.T) {
got := Max(1, 2)
if got != 2 {
t.Errorf("Max(1, 2) = %f; want 2", got)
}
}
執(zhí)行go test
命令來執(zhí)行如上單元測試用例:
$ go test
PASS
ok github.com/marmotedu/gopractise-demo/31/test 0.002s
go test
命令自動搜集所有的測試文件,也就是格式為*_test.go
的文件,從中提取全部測試函數并執(zhí)行。
go test還支持下面三個參數。
- -v,顯示所有測試函數的運行細節(jié):
$ go test -v
=== RUN TestAbs
--- PASS: TestAbs (0.00s)
=== RUN TestMax
--- PASS: TestMax (0.00s)
PASS
ok github.com/marmotedu/gopractise-demo/31/test 0.002s
- -run < regexp>,指定要執(zhí)行的測試函數:
$ go test -v -run='TestA.*'
=== RUN TestAbs
--- PASS: TestAbs (0.00s)
PASS
ok github.com/marmotedu/gopractise-demo/31/test 0.001s
上面的例子中,我們只運行了以TestA
開頭的測試函數。
- -count N,指定執(zhí)行測試函數的次數:
$ go test -v -run='TestA.*' -count=2
=== RUN TestAbs
--- PASS: TestAbs (0.00s)
=== RUN TestAbs
--- PASS: TestAbs (0.00s)
PASS
ok github.com/marmotedu/gopractise-demo/31/test 0.002s
多個輸入的測試用例
前面介紹的單元測試用例只有一個輸入,但是很多時候,我們需要測試一個函數在多種不同輸入下是否能正常返回。這時候,我們可以編寫一個稍微復雜點的測試用例,用來支持多輸入下的用例測試。例如,我們可以將TestAbs
改造成如下函數:
func TestAbs_2(t *testing.T) {
tests := []struct {
x float64
want float64
}{
{-0.3, 0.3},
{-2, 2},
{-3.1, 3.1},
{5, 5},
}
for _, tt := range tests {
if got := Abs(tt.x); got != tt.want {
t.Errorf("Abs() = %f, want %v", got, tt.want)
}
}
}
上述測試用例函數中,我們定義了一個結構體數組,數組中的每一個元素代表一次測試用例。數組元素的的值包含輸入和預期的返回值:
tests := []struct {
x float64
want float64
}{
{-0.3, 0.3},
{-2, 2},
{-3.1, 3.1},
{5, 5},
}
上述測試用例,將被測函數放在for循環(huán)中執(zhí)行:
for _, tt := range tests {
if got := Abs(tt.x); got != tt.want {
t.Errorf("Abs() = %f, want %v", got, tt.want)
}
}
?
上面的測試用例中,我們通過got != tt.want
來對比實際返回結果和預期返回結果。我們也可以使用github.com/stretchr/testify/assert
包中提供的函數來做結果對比,例如:
func TestAbs_3(t *testing.T) {
tests := []struct {
x float64
want float64
}{
{-0.3, 0.3},
{-2, 2},
{-3.1, 3.1},
{5, 5},
}
for _, tt := range tests {
got := Abs(tt.x)
assert.Equal(t, got, tt.want)
}
}
使用assert
來對比結果,有下面這些好處:
- 友好的輸出結果,易于閱讀。
- 因為少了
if got := Xxx(); got != tt.wang {}
的判斷,代碼變得更加簡潔。 - 可以針對每次斷言,添加額外的消息說明,例如
assert.Equal(t, got, tt.want, "Abs test")
。
assert包還提供了很多其他函數,供開發(fā)者進行結果對比,例如Zero
、NotZero
、Equal
、NotEqual
、Less
、True
、Nil
、NotNil
等。如果想了解更多函數,你可以參考go doc github.com/stretchr/testify/assert
。
自動生成單元測試用例
通過上面的學習,你也許可以發(fā)現,測試用例其實可以抽象成下面的模型:
用代碼可表示為:
func TestXxx(t *testing.T) {
type args struct {
// TODO: Add function input parameter definition.
}
type want struct {
// TODO: Add function return parameter definition.
}
tests := []struct {
name string
args args
want want
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Xxx(tt.args); got != tt.want {
t.Errorf("Xxx() = %v, want %v", got, tt.want)
}
})
}
}
既然測試用例可以抽象成一些模型,那么我們就可以基于這些模型來自動生成測試代碼。Go社區(qū)中有一些優(yōu)秀的工具可以自動生成測試代碼,我推薦你使用gotests工具。
下面,我來講講gotests工具的使用方法,可以分成三個步驟。
第一步,安裝gotests工具:
$ go get -u github.com/cweill/gotests/...
gotests命令執(zhí)行格式為:gotests [options] [PATH] [FILE] ...
。gotests可以為PATH
下的所有Go源碼文件中的函數生成測試代碼,也可以只為某個FILE
中的函數生成測試代碼。
第二步,進入測試代碼目錄,執(zhí)行gotests生成測試用例:
$ gotests -all -w .
?
第三步,添加測試用例:
生成完測試用例,你只需要添加需要測試的輸入和預期的輸出就可以了。下面的測試用例是通過gotests生成的:
func TestUnpointer(t *testing.T) {
type args struct {
offset *int64
limit *int64
}
tests := []struct {
name string
args args
want *LimitAndOffset
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Unpointer(tt.args.offset, tt.args.limit); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Unpointer() = %v, want %v", got, tt.want)
}
})
}
}
?補全后的測試用例見gorm_test.go文件。
性能測試
?
性能測試的用例函數必須以Benchmark
開頭,例如BenchmarkXxx
或Benchmark_Xxx
(?Xxx
?部分為任意字母數字組合,首字母大寫)。
函數參數必須是*testing.B
,函數內以b.N
作為循環(huán)次數,其中N
會在運行時動態(tài)調整,直到性能測試函數可以持續(xù)足夠長的時間,以便能夠可靠地計時。下面的代碼是一個簡單的性能測試函數(函數位于文件math_test.go中):
func BenchmarkRandInt(b *testing.B) {
for i := 0; i < b.N; i++ {
RandInt()
}
}
go test
命令默認不會執(zhí)行性能測試函數,需要通過指定參數-bench <pattern>
來運行性能測試函數。-bench
后可以跟正則表達式,選擇需要執(zhí)行的性能測試函數,例如go test -bench=".*"
表示執(zhí)行所有的壓力測試函數。執(zhí)行go test -bench=".*"
后輸出如下:
$ go test -bench=".*"
goos: linux
goarch: amd64
pkg: github.com/marmotedu/gopractise-demo/31/test
BenchmarkRandInt-4 97384827 12.4 ns/op
PASS
ok github.com/marmotedu/gopractise-demo/31/test 1.223s
上面的結果只顯示了性能測試函數的執(zhí)行結果。BenchmarkRandInt
性能測試函數的執(zhí)行結果如下:
BenchmarkRandInt-4 90848414 12.8 ns/op
每個函數的性能執(zhí)行結果一共有3列,分別代表不同的意思,這里用上面的函數舉例子:
BenchmarkRandInt-4
,BenchmarkRandInt
表示所測試的測試函數名,4表示有4個CPU線程參與了此次測試,默認是GOMAXPROCS
的值。90848414
?,說明函數中的循環(huán)執(zhí)行了90848414
次。12.8 ns/op
,說明每次循環(huán)的執(zhí)行平均耗時是?12.8
?納秒,該值越小,說明代碼性能越高。
如果我們的性能測試函數在執(zhí)行循環(huán)前,需要做一些耗時的準備工作,我們就需要重置性能測試時間計數,例如:
func BenchmarkBigLen(b *testing.B) {
big := NewBig()
b.ResetTimer()
for i := 0; i < b.N; i++ {
big.Len()
}
}
當然,我們也可以先停止性能測試的時間計數,然后再開始時間計數,例如:
func BenchmarkBigLen(b *testing.B) {
b.StopTimer() // 調用該函數停止壓力測試的時間計數
big := NewBig()
b.StartTimer() // 重新開始時間
for i := 0; i < b.N; i++ {
big.Len()
}
}
B類型的性能測試還支持下面 4 個參數。
- benchmem,輸出內存分配統(tǒng)計:
$ go test -bench=".*" -benchmem
goos: linux
goarch: amd64
pkg: github.com/marmotedu/gopractise-demo/31/test
BenchmarkRandInt-4 96776823 12.8 ns/op 0 B/op 0 allocs/op
PASS
ok github.com/marmotedu/gopractise-demo/31/test 1.255s
指定了-benchmem
參數后,執(zhí)行結果中又多了兩列: 0 B/op,表示每次執(zhí)行分配了多少內存(字節(jié)),該值越小,說明代碼內存占用越?。? allocs/op,表示每次執(zhí)行分配了多少次內存,該值越小,說明分配內存次數越少,意味著代碼性能越高。
- benchtime,指定測試時間和循環(huán)執(zhí)行次數(格式需要為Nx,例如100x):
$ go test -bench=".*" -benchtime=10s # 指定測試時間
goos: linux
goarch: amd64
pkg: github.com/marmotedu/gopractise-demo/31/test
BenchmarkRandInt-4? ? ? 910328618? ? ? ? ? ? ? ?13.1 ns/op
PASS
ok? ? ? github.com/marmotedu/gopractise-demo/31/test? ? 13.260s
$ go test -bench=".*" -benchtime=100x # 指定循環(huán)執(zhí)行次數
goos: linux
goarch: amd64
pkg: github.com/marmotedu/gopractise-demo/31/test
BenchmarkRandInt-4? ? ? ? ? ?100? ? ? ? ? ? ? ? 16.9 ns/op
PASS
ok? ? ? github.com/marmotedu/gopractise-demo/31/test? ? 0.003s
- cpu,指定GOMAXPROCS。
- timeout,指定測試函數執(zhí)行的超時時間:
$ go test -bench=".*" -timeout=10s
goos: linux
goarch: amd64
pkg: github.com/marmotedu/gopractise-demo/31/test
BenchmarkRandInt-4 97375881 12.4 ns/op
PASS
ok github.com/marmotedu/gopractise-demo/31/test 1.224s
總結
代碼開發(fā)完成之后,我們需要為代碼編寫單元測試用例,并根據需要,給一些函數編寫性能測試用例。Go語言提供了?testing
?包,供我們編寫測試用例,并通過?go test
?命令來執(zhí)行這些測試用例。
go test在執(zhí)行測試用例時,會查找具有固定格式的Go源碼文件名,并執(zhí)行其中具有固定格式的函數,這些函數就是測試用例。這就要求我們的測試文件名、函數名要符合?go test
?工具的要求:Go的測試文件名必須以?_test.go
?結尾;測試用例函數必須以?Test
?、?Benchmark
?、?Example
?開頭。此外,我們在編寫測試用例時,還要注意包和變量的命名規(guī)范。
Go項目開發(fā)中,編寫得最多的是單元測試用例。單元測試用例函數以?Test
?開頭,例如?TestXxx
?或?Test_xxx
?(Xxx
?部分為任意字母數字組合,首字母大寫)。函數參數必須是?*testing.T
?,可以使用該類型來記錄錯誤或測試狀態(tài)。我們可以調用?testing.T
?的?Error
?、Errorf
?、FailNow
?、Fatal
?、FatalIf
?方法,來說明測試不通過;調用?Log
?、Logf
?方法來記錄測試信息。
下面是一個簡單的單元測試函數:
func TestAbs(t *testing.T) {
? ? got := Abs(-1)
? ? if got != 1 {
? ? ? ? t.Errorf("Abs(-1) = %f; want 1", got)
? ? }
}
編寫完測試用例之后,可以使用?go test
?命令行工具來執(zhí)行這些測試用例。
此外,我們還可以使用gotests工具,來自動地生成單元測試用例,從而減少編寫測試用例的工作量。文章來源:http://www.zghlxwxcb.cn/news/detail-847581.html
我們在Go項目開發(fā)中,還經常需要編寫性能測試用例。性能測試用例函數必須以Benchmark
開頭,以*testing.B
?作為函數入參,通過?go test -bench <pattern>
?運行。文章來源地址http://www.zghlxwxcb.cn/news/detail-847581.html
課后練習
- 編寫一個?
PrintHello
?函數,該函數會返回?Hello World
?字符串,并編寫單元測試用例,對?PrintHello
?函數進行測試。 - 思考一下,哪些場景下采用白盒測試,哪些場景下采用黑盒測試?
到了這里,關于36-代碼測試(上):如何編寫Go語言單元測試和性能測試用例?的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!