一.單元測試
? ? ? ? 1.1 go test工具
? ? ? ? go語言中的測試依賴go test命令。編寫測試代碼和編寫普通的Go代碼過程類似,并不需要學(xué)習(xí)新的語法,規(guī)則和工具。
? ? ? ? go test命令是一個(gè)按照一定約定和組織的測試代碼的驅(qū)動程序。在包目錄內(nèi),所有以_test.go為后綴名的源代碼文件都是go test測試的一部分,不會被go build編譯到最終可執(zhí)行文件中。
? ? ? ? 在*_test.go文件中有三種類型的函數(shù),單元測試函數(shù),基準(zhǔn)測試函數(shù)和示例函數(shù)。
? ? ? ? ?go test命令會遍歷所有的*_test.go文件中符合上述命令規(guī)則的函數(shù),然后生成一個(gè)臨時(shí)的main包用于調(diào)用相應(yīng)的測試函數(shù),然后構(gòu)建并運(yùn)行,報(bào)告測試結(jié)果,最后清理測試中生成的臨時(shí)文件。
? ? ? ? Golang單元測試對文件名和方法名都有很嚴(yán)格的要求:
- 文件名必須以xx_test.go命名
- 方法必須是Test[^a-z]開頭
- 方法參數(shù)必須是t *testing.T類型
- 使用go test執(zhí)行單元測試
go test的參數(shù)解讀:
- go test是go語言自帶的測試工具,其中包含的是兩類,單元測試和性能測試
- 通過go help test可以看到go test的使用說明。
格式:
go test [-c] [-i] [build flags] [package] [flags for test binary]
參數(shù)解讀:
-c:編譯go test成為可執(zhí)行二進(jìn)制文件,但是不運(yùn)行測試
-i:安裝測試包依賴的package,但是不運(yùn)行測試
build flags:調(diào)用go help build,這些都是編譯運(yùn)行過程中需要使用到的參數(shù),一般設(shè)置為空。
packages:調(diào)用go help packages,這些事關(guān)于包的管理,一般設(shè)置為空。
關(guān)于flags for test binary,調(diào)用go help testflag,這些是go test過程中經(jīng)常使用到的參數(shù)
? ? ? ? -test.v:是否輸出全部的單元測試用例(不管成功失敗),默認(rèn)沒有加上,所以只輸出失敗的單元測試用例。
????????-test.run pattern: 只跑哪些單元測試用例????????-test.bench patten: 只跑那些性能測試用例????????-test.benchmem : 是否在性能測試的時(shí)候輸出內(nèi)存情況????????-test.benchtime t : 性能測試運(yùn)行的時(shí)間,默認(rèn)是 1s????????-test.cpuprofile cpu.out : 是否輸出 cpu 性能分析文件????????-test.memprofile mem.out : 是否輸出內(nèi)存性能分析文件????????-test.blockprofile block.out : 是否輸出內(nèi)部 goroutine 阻塞的性能分析文件????????-test.memprofilerate n : 內(nèi)存性能分析的時(shí)候有一個(gè)分配了多少的時(shí)候才打點(diǎn)記錄的問題。這 個(gè)參數(shù)就是設(shè)置打點(diǎn)的內(nèi)存分配間隔,也就是profile 中一個(gè) sample 代表的內(nèi)存大小。默認(rèn)是設(shè)置為 512 * 1024的。如果你將它設(shè)置為 1 ,則每分配一個(gè)內(nèi)存塊就會在 profile 中有個(gè)打點(diǎn),那么生成的 profile的 sample 就會非常多。如果你設(shè)置為 0 ,那就是不做打點(diǎn)了。 你可以通過設(shè)置memprofilerate=1 和 GOGC=off 來關(guān)閉內(nèi)存回收,并且對每個(gè)內(nèi)存塊的分配進(jìn)行觀察。????????-test.blockprofilerate n: 基本同上,控制的是 goroutine 阻塞時(shí)候打點(diǎn)的納秒數(shù)。默認(rèn)不設(shè) 置就相當(dāng)于-test.blockprofilerate=1 ,每一納秒都打點(diǎn)記錄一下????????-test.parallel n : 性能測試的程序并行 cpu 數(shù),默認(rèn)等于 GOMAXPROCS 。????????-test.timeout t : 如果測試用例運(yùn)行時(shí)間超過 t ,則拋出 panic????????-test.cpu 1,2,4 : 程序運(yùn)行在哪些 CPU 上面,使用二進(jìn)制的 1 所在位代表,和 nginx 的 nginx_worker_cpu_affinity是一個(gè)道理????????-test.short : 將那些運(yùn)行時(shí)間較長的測試用例運(yùn)行時(shí)間縮短
目錄結(jié)構(gòu):
? ? ? ? 1.2 測試函數(shù)
? ? ? ? 1.2.1 測試函數(shù)格式
? ? ? ? 每個(gè)測試函數(shù)必須導(dǎo)入testing包,測試函數(shù)的基本格式如下:?
func TestName(t *testing.T){
//...
}
? ? ? ? ?測試函數(shù)的名字必須以Test開頭,可選的后綴名必須以大寫字母開頭。舉幾個(gè)例子:
func TestAdd(t *testing.T){...}
func TestSum(t *testing.T){...}
func TestLog(t *testing.T){...}
? ? ? ? 其中參數(shù)t用于報(bào)告測試失敗和附加的日志信息。testing.T的擁有的方法如下:
func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (t *T) Parallel()
func (t *T) Run(name string, f func(t *T)) bool
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool
? ? ? ? ?1.2.2 測試函數(shù)示例
? ? ? ? 一個(gè)軟件程序由很多單元組件構(gòu)成。單元組件可以是函數(shù),結(jié)構(gòu)體,方法和最終用戶可能依賴的東西??傊覀冃枰_保這些組件是能夠正常運(yùn)行。單元測試是一些利用各種方法測試單元組件的程序,它會將結(jié)果與預(yù)期輸出進(jìn)行比較。
????????目錄結(jié)構(gòu):
? ? ? ? split.go代碼:
package split
import "strings"
func Split(s, seq string) (res []string) {
i := strings.Index(s, seq)
for i > -1 {
res = append(res, s[:i])
s = s[i+1:]
i = strings.Index(s, seq)
}
res = append(res, s)
return
}
? ? ? ? split_test.go代碼:
package split
import (
"reflect"
"testing"
)
func TestSplit(t *testing.T) { //測試函數(shù)必須以Test開頭,必須接收一個(gè)*testing.T的類型參數(shù)
got := Split("a:b:c", ":") //測試程序的輸出
want := []string{"a", "b", "c"} //期待的結(jié)果
if !reflect.DeepEqual(got, want) { //slice不能進(jìn)行比較,借助反射包中的方法進(jìn)行比較
t.Errorf("excepted:%#v, got:%#v", want, got)
}
}
? ? ? ? 執(zhí)行g(shù)o test:
? ? ? ? 再增加一個(gè)測試用例:
func TestMoreSplit(t *testing.T) {
got := Split("abcd", "bc")
want := []string{"a", "d"}
if !reflect.DeepEqual(got, want) {
t.Errorf("excepted:%#v, got:%#v", want, got)
}
}
? ? ? ? 輸入go test和go test -v查看測試函數(shù)和運(yùn)行時(shí)間:
? ? ? ? 還可以在go test命令后面加-run參數(shù),它對應(yīng)一個(gè)正則表達(dá)式,只有函數(shù)名匹配上的測試函數(shù)才會被go test命令執(zhí)行。
? ? ? ? 代碼中失敗的原因是:在Split函數(shù)并沒有考慮seq為多字符的情況,下面為修復(fù)之后的代碼:
package split
import "strings"
func Split(s, seq string) (res []string) {
i := strings.Index(s, seq)
for i > -1 {
res = append(res, s[:i])
s = s[i+len(seq):]
i = strings.Index(s, seq)
}
res = append(res, s)
return
}
? ? ? ? 1.2.3 測試組
package split
import (
"reflect"
"testing"
)
func TestSplit(t *testing.T) {
//定義測試用例類型
type test struct {
input string
seq string
output []string
}
//定義存儲測試用例的切片
tests := []test{
{"a:b:c", ":", []string{"a", "b", "c"}},
{"a:b:c", ",", []string{"a:b:c"}},
{"abcd", "bc", []string{"a", "d"}},
{"枯藤老樹昏鴉", "老", []string{"枯藤", "樹昏鴉"}},
}
//遍歷切片,逐一執(zhí)行測試用例
for i, v := range tests {
res := Split(v.input, v.seq)
if !reflect.DeepEqual(v.output, res) {
t.Errorf("index:%#v, excepted:%#v, got:%#v", i, v.output, v.input)
}
}
}
? ? ? ? 1.2.4 子測試
? ? ? ? 如果測試用例比較多的時(shí)候,我們沒有辦法一眼看出具體是哪一個(gè)測試用例失敗了,我們可以使用下面的解決辦法,使用testing.T的Run方法。
package split
import (
"reflect"
"testing"
)
func TestSplit(t *testing.T) {
//定義測試用例類型
type test struct {
input string
seq string
output []string
}
tests := map[string]test{ //測試用例使用map儲存
"simple": {"a:b:c", ":", []string{"a", "b", "c"}},
"wrong seq": {"a:b:c", ",", []string{"a:b:c"}},
"more seq": {"abcd", "bc", []string{"a", "d"}},
"leading seq": {"枯藤老樹昏鴉", "老", []string{"枯藤", "樹昏鴉"}},
}
for n, v := range tests {
t.Run(n, func(t *testing.T) { //使用t.Runc()執(zhí)行子測試
res := Split(v.input, v.seq)
if !reflect.DeepEqual(v.output, res) {
t.Errorf("excepted:%#v, got:%#v", v.output, v.input)
}
})
}
}
? ? ? ? 1.2.5 測試覆蓋率
?????????測試覆蓋率是你的代碼被測試套件的百分比,通常我們使用的是語句的覆蓋率。也就是在測試中,至少被運(yùn)行一次的代碼占總代碼的比例。
? ? ? ? Go提供內(nèi)置功能來檢查你的代碼的覆蓋率。我們可以使用go test -cover來查看測試覆蓋率。
例子還是上面的例子:
? ? ? ? 從上面的結(jié)果可以看到我們的測試用例覆蓋了100%的代碼。
? ? ? ? Go還提供了一個(gè)額外的-coverprofile參數(shù),用來將覆蓋率相關(guān)的記錄信息輸出到一個(gè)文件。例如:
? ? ? ? 上面的命令會將覆蓋率相關(guān)的信息輸出到當(dāng)前文件夾下面的c.out文件中,然后我們執(zhí)行g(shù)o tool cover -html=c.out,使用cover工具來處理生成的記錄信息,該命令會打開本地的瀏覽器窗口生成一個(gè)HTML報(bào)告。
二.壓力測試
? ? ? ? 2.1 基準(zhǔn)測試
? ? ? ? 基準(zhǔn)測試就是在一定的工作負(fù)載之下檢測程序性能的一種方法?;鶞?zhǔn)測試的基本格式如下:
func BenchmarkName(b *testing.B){
//...
}
? ? ? ? 基準(zhǔn)測試以Benchmark為前綴,需要一個(gè)*testing.B類型的參數(shù)b,基準(zhǔn)測試必須要執(zhí)行b.N次,這樣測試才有對照性,b.N的值是系統(tǒng)根據(jù)實(shí)際情況去調(diào)整的,從而保證測試的穩(wěn)定性。
? ? ? ? testing.B擁有的方法如下:
func (c *B) Error(args ...interface{})
func (c *B) Errorf(format string, args ...interface{})
func (c *B) Fail()
func (c *B) FailNow()
func (c *B) Failed() bool
func (c *B) Fatal(args ...interface{})
func (c *B) Fatalf(format string, args ...interface{})
func (c *B) Log(args ...interface{})
func (c *B) Logf(format string, args ...interface{})
func (c *B) Name() string
func (b *B) ReportAllocs()
func (b *B) ResetTimer()
func (b *B) Run(name string, f func(b *B)) bool
func (b *B) RunParallel(body func(*PB))
func (b *B) SetBytes(n int64)
func (b *B) SetParallelism(p int)
func (c *B) Skip(args ...interface{})
func (c *B) SkipNow()
func (c *B) Skipf(format string, args ...interface{})
func (c *B) Skipped() bool
func (b *B) StartTimer()
func (b *B) StopTimer()
? ? ? ? 2.2 基準(zhǔn)測試示例
? ? ? ? 我們?yōu)镾plit包中的Split函數(shù)編寫基準(zhǔn)測試如下:
func BenchmarkSplit(b *testing.B) {
for i := 0; i < b.N; i++ {
Split("枯藤老樹昏鴉", "老")
}
}
? ? ? ? 基準(zhǔn)測試并不會默認(rèn)執(zhí)行,需要增加-bench參數(shù),所以我們通過執(zhí)行g(shù)o test -bench=Split命令執(zhí)行基準(zhǔn)測試:
? ? ? ? 其中BenchmarkSplit-4表示對Split函數(shù)進(jìn)行基準(zhǔn)測試,數(shù)字4表示GOMAXPROCS的值,這個(gè)對于并發(fā)基準(zhǔn)測試很重要。8877892和128.4ns/op表示每次調(diào)用Split函數(shù)耗時(shí)128.4ns,這個(gè)結(jié)果是8877892次調(diào)用的平均值。
? ? ? ? 我們也可以加上-banchmem參數(shù),來獲取內(nèi)存分配的統(tǒng)計(jì)數(shù)據(jù)。
? ? ? ? 其中,48B/op表示每次操作內(nèi)存分配了48字節(jié),2allocs/op則表示每次操作進(jìn)行了2次內(nèi)存分配。
????????我們將Split函數(shù)優(yōu)化如下: 提前使用make函數(shù)將res初始化為一個(gè)容量足夠大的切片,而不再像之前一樣通過調(diào)用append函數(shù)來追加,append可能會導(dǎo)致擴(kuò)容,內(nèi)存重新分配。我們來看一下性能會有多少提升。
func Split(s, seq string) (res []string) {
res = make([]string, 0, strings.Count(s, seq)+1) //提前分配好內(nèi)存
i := strings.Index(s, seq)
for i > -1 {
res = append(res, s[:i])
s = s[i+len(seq):]
i = strings.Index(s, seq)
}
res = append(res, s)
return
}
? ? ? ? 2.3 性能比較函數(shù)
? ? ? ? 上面的基準(zhǔn)測試只能得到給定操作的絕對耗時(shí),但是在很多性能問題是發(fā)生在兩個(gè)不同的操作之間的相對耗時(shí)。比如:同一個(gè)函數(shù)處理1000個(gè)元素的耗時(shí)與處理1萬甚至100萬個(gè)元素耗時(shí)差別是多少?再或者對于同一個(gè)任務(wù)究竟使用哪種算法性能最佳?我們通常需要對兩個(gè)不同的算法的實(shí)現(xiàn)使用相同的輸入來進(jìn)行基準(zhǔn)比較測試。
? ? ? ? 性能比較函數(shù)通常是一個(gè)帶有參數(shù)的函數(shù),被多個(gè)不同的Brenchmark函數(shù)傳入不同的值來調(diào)用。舉個(gè)例子:
func brenchmark(b *testing.B, size int){/*...*/}
func Brenchmark10(b *testing.B){ brenchmark(b, 10) }
func Brenchmark100(b *testing.B){ brenchmark(b, 100) }
func Brenchmark1000(b *testing.B){ brenchmark(b, 1000) }
? ? ? ? ?例如:我們編寫一個(gè)斐波那契函數(shù):
package fib
func Fib(n int) int {
if n < 2 {
return n
}
return Fib(n-1) + Fib(n-2)
}
? ? ? ? 性能比較函數(shù):
? ? ? ? 運(yùn)行基準(zhǔn)測試:
? ? ? ? ? ? ? ? 需要注意的是,默認(rèn)情況下,每個(gè)基準(zhǔn)測試至少運(yùn)行一秒(即總共時(shí)間得字少大于等于1秒),如果Benchmark函數(shù)返回時(shí),沒有到1秒,則b.N的值會按1,2,5,10,20...增加,并且函數(shù)再次運(yùn)行。
? ? ? ? 最終BenchmarkFib40只運(yùn)行了兩次,沒運(yùn)行的平均時(shí)間只有不到1秒。像這種情況我們應(yīng)該使用-brenchtime標(biāo)志增加最小基準(zhǔn)時(shí)間,以產(chǎn)生更準(zhǔn)確的結(jié)果。
? ? ? ? ?這樣次數(shù)增加到了42次,結(jié)果就會更加準(zhǔn)確了。
使用性能比較函數(shù)做測試的時(shí)候一個(gè)容易犯的錯(cuò)誤是把b.N作為輸入的大小,下面兩個(gè)例子都是錯(cuò)誤示范:
//錯(cuò)誤示范1 func BenchmarkFibWrong1(b *testing.B){ for i := 0; i < b.N; i++{ Fib(i) } } //錯(cuò)誤釋放2 func BenchmarkFibWrong2(c *testing.B){ Fib(b.N) }
? ? ? ? ?2.4 重置時(shí)間
? ? ? ? b.ResetTimer之前的處理不會放到執(zhí)行時(shí)間里,也不會輸出到報(bào)告中,所以可以在之前做一些不計(jì)劃走位測試報(bào)告的操作。
func BenchmarkSplit(b *testing.B) {
time.Sleep(5 * time.Second) //做一些耗時(shí)的無關(guān)操作
b.ResetTimer() //重置計(jì)時(shí)器
for i := 0; i < b.N; i++ {
Split("枯藤老樹昏鴉", "老")
}
}
? ? ? ? 2.5 并行測試
? ? ? ? func (b B) RunParallel (body func(PB))會以并行的方式執(zhí)行給定的基準(zhǔn)測試。
? ? ? ? RunParallel會創(chuàng)建出多個(gè)goroutine,并將b.N分配給這些goroutine執(zhí)行,其中g(shù)oroutine數(shù)量的默認(rèn)值為GOMAXPROCS。用戶如果想要增加非CPU受限(non-CPU-bound)基準(zhǔn)測試的并行性,那么可以在RunParallel之前調(diào)用SetParallelism。RunParallel通常會與-cpu標(biāo)志一同使用。?
func BenchmarkSplitParallel(b *testing.B) {
//b.SetParallelism(1) 設(shè)置使用CPU數(shù)量
b.RunParallel(func(pb *testing.PB) {
for pb.Next() { //Next報(bào)告是否還有更多的迭代要執(zhí)行。
Split("枯藤老樹昏鴉", "老")
}
})
}
? ? ? ? 還可以通過在測試命令后面加-cpu參數(shù)如go test -bench=. -cpu 1來指定使用的CPU數(shù)量。
? ? ? ? ? 2.6 Setup和TearDown
? ? ? ? 測試程序有時(shí)需要在測試之前進(jìn)行額外的設(shè)置(setup)或在測試之后進(jìn)行拆卸(teardown)。
? ? ? ? 2.6.1 TestMain
? ? ? ? 通過在*_test.go文件中定義TestMain函數(shù)可以在測試之前進(jìn)行額外的設(shè)置(setup)或在測試之后進(jìn)行拆卸(teardown)操作。
? ? ? ? 如果測試文件包含函數(shù):func TestMain(m *testing.M)那么生成測試會先調(diào)用TestMain(m),然后再運(yùn)行具體測試。TestMain運(yùn)行在主goroutine中,可以在調(diào)用m.Run前后做任何設(shè)置(setup)和拆卸(teardown)。退出測試的時(shí)候應(yīng)該使用m.Run的返回值作為參數(shù)調(diào)用的os.Exit。
? ? ? ? 一個(gè)TestMain來設(shè)置Setup和TearDown的示例如下:
func TestMain(m *testing.M) {
fmt.Println("write setup code here ...") //測試之前做的一些設(shè)置
//如果TestMain使用了flags,這里應(yīng)該加上flags.Parse()
retCode := m.Run() //執(zhí)行測試
fmt.Println("write teardown code here ...") //測試之后的拆卸工作
os.Exit(retCode) //退出測試
}
? ? ? ? ?需要注意的是:在調(diào)用TestMain時(shí),flag.Parse并沒有被調(diào)用。所以如果TestMain依賴于(命令行參數(shù))command-line標(biāo)志(包括testing包的標(biāo)記),則應(yīng)該先調(diào)用flag.Parse。
? ? ? ? 2.6.2 子測試的Setup與Teardown
? ? ? ? 有時(shí)我們可能需要為每個(gè)測試集設(shè)置Setup和Teardown,也有可能需要為每個(gè)子測試設(shè)置Setup與Teardown。下面我們定義兩個(gè)工具函數(shù)如下:
// 測試集的setup和teardown
func setupTestCase(t *testing.T) func(t *testing.T) {
t.Log("如果需要在此執(zhí)行:測試之前的setup")
return func(t *testing.T) {
t.Log("如果需要在此執(zhí)行:測試之前的teardown")
}
}
// 子測試的setup和teardown
func setupSubTest(t *testing.T) func(t *testing.T) {
t.Log("如果需要在此執(zhí)行:子測試之前的setup")
return func(t *testing.T) {
t.Log("如果需要在此執(zhí)行:子測試之前的teardown")
}
}
? ? ? ? ?使用方式:
func TestSplit(t *testing.T) {
//定義測試用例類型
type test struct {
input string
seq string
output []string
}
tests := map[string]test{ //測試用例使用map儲存
"simple": {"a:b:c", ":", []string{"a", "b", "c"}},
"wrong seq": {"a:b:c", ",", []string{"a:b:c"}},
"more seq": {"abcd", "bc", []string{"a", "d"}},
"leading seq": {"枯藤老樹昏鴉", "老", []string{"枯藤", "樹昏鴉"}},
}
teardownTestCase := setupTestCase(t) //測試之前執(zhí)行setup操作
defer teardownTestCase(t) //測試之后執(zhí)行testsown操作
//遍歷切片,逐一執(zhí)行測試用例
for n, v := range tests {
teardownSubTest := setupSubTest(t) //子測試之前的setup操作
defer teardownSubTest(t) //子測試之后的teardown操作
t.Run(n, func(t *testing.T) { //使用t.Runc()執(zhí)行子測試
res := Split(v.input, v.seq)
if !reflect.DeepEqual(v.output, res) {
t.Errorf("excepted:%#v, got:%#v", v.output, v.input)
}
})
}
}
? ? ? ? 測試結(jié)果:
三.示例函數(shù)
? ? ? ? 3.1 示例函數(shù)格式
? ? ? ? 被go test特殊對待的第三中函數(shù)就是示例函數(shù),它們的函數(shù)名以Example為前綴。既沒有參數(shù)也沒有返回值。格式如下:
func ExampleName(){
//...
}
? ? ? ? 3.2 示例函數(shù)示例
func ExampleSplit() {
fmt.Println(Split("a:b:c", ":"))
fmt.Println(Split("枯藤老樹昏鴉", "老"))
}
? ? ? ? ?為你的代碼編寫示例函數(shù)又下面三個(gè)用處:
- 示例函數(shù)能夠作為文檔使用,例如基于web的godoc中能把示例函數(shù)與對應(yīng)的函數(shù)或包相關(guān)聯(lián)。
- 示例函數(shù)只要包含了,也可以通過go test運(yùn)行可執(zhí)行測試。
- go test -run Example
- 示例函數(shù)提供了可以直接運(yùn)行的示例代碼,可以直接在golang.org的godoc文檔服務(wù)器上使用GoPlayground運(yùn)行實(shí)例代碼。下圖為string.ToUpper函數(shù)在Playground的實(shí)例函數(shù)效果。
?文章來源地址http://www.zghlxwxcb.cn/news/detail-859498.html
?文章來源:http://www.zghlxwxcb.cn/news/detail-859498.html
?
?
?
?
?????????
?
?
到了這里,關(guān)于Golang單元測試和壓力測試的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!