Go自帶測(cè)試框架
單元測(cè)試
- 建議Go 語(yǔ)言推薦測(cè)試文件和源代碼文件放在一塊,測(cè)試文件以 _test.go 結(jié)尾。
- 函數(shù)名必須以 Test 開(kāi)頭,后面一般跟待測(cè)試的函數(shù)名
- 參數(shù)為 t *testing.T
簡(jiǎn)單測(cè)試用例定義如下:
func TestXXXX(t *testing.T) {
// ...
}
在goland中,編寫(xiě)好方法后,右鍵Generate->Test for funtion, 可自動(dòng)生成單元測(cè)試代碼
生成的代碼如下:
需要在TODO里填上單元測(cè)試參數(shù),含義如下:
name:?jiǎn)卧獪y(cè)試名稱(chēng)
args:方法入?yún)? want:希望的出參
測(cè)試結(jié)果:
日志打印
Log() | 打印日志 |
---|---|
Logf() | 格式化打印日志 |
Error() | 打印錯(cuò)誤日志 |
Errorf() | 格式化打印錯(cuò)誤日志 |
Fatal() | 打印致命日志, 會(huì)直接中斷當(dāng)前測(cè)試方法 |
Fatalf() | 格式化打印致命日志,會(huì)直接中斷當(dāng)前測(cè)試方法 |
Fail() | 標(biāo)記失敗,但繼續(xù)執(zhí)行當(dāng)前測(cè)試函數(shù) |
FailNow() | 失敗,立即終止當(dāng)前測(cè)試函數(shù)執(zhí)行 |
Skip() | 跳過(guò)當(dāng)前函數(shù),通常用于未完成的測(cè)試用例 |
基準(zhǔn)測(cè)試
基準(zhǔn)測(cè)試用例的定義如下:
func BenchmarkName(b *testing.B){
// ...
}
- 函數(shù)名必須以 Benchmark 開(kāi)頭,后面一般跟待測(cè)試的函數(shù)名
- 參數(shù)為 b *testing.B
- goland中沒(méi)有自動(dòng)基準(zhǔn)測(cè)試的方法,需要按照規(guī)則手動(dòng)自己加
原方法
func sayHi(name string) string{
return "hi," + name
}
基準(zhǔn)測(cè)試代碼
func BenchmarkSayHi(b *testing.B) {
for i := 0; i < b.N; i++ {
sayHi("Max")
}
}
Goland中執(zhí)行基準(zhǔn)測(cè)試
命令行中執(zhí)行基準(zhǔn)測(cè)試
go test helloworld_test.go
結(jié)果解讀
當(dāng)測(cè)試開(kāi)始時(shí),b.N的值被設(shè)置為1,執(zhí)行后如果沒(méi)有超過(guò)默認(rèn)執(zhí)行時(shí)間上限(默認(rèn)為1秒),則加大b.N的值,按某種規(guī)則一直遞增,直到執(zhí)行時(shí)間等于或超過(guò)上限,那么就用這一次的b.N的值,做為測(cè)試的最終結(jié)果
BenchmarkSayHi-12 81593520 14.71 ns/op
PASS
ok zh.com/internal/benchmark_test 2.347s
- BenchmarkSayHi-12表示執(zhí)行 BenchmarkSayHi 時(shí),所用的最大P的數(shù)量為12
- 81593520: 表示sayHi()方法在達(dá)到這個(gè)執(zhí)行次數(shù)時(shí),等于或超過(guò)了1秒
- 14.71 ns/op: 表示每次執(zhí)行sayHi()所消耗的平均執(zhí)行時(shí)間
- 2.347s:表示測(cè)試總共用時(shí)
測(cè)試總時(shí)間的計(jì)算
既然81593520表示1秒或大于1秒時(shí)執(zhí)行的次數(shù),那么測(cè)試總時(shí)間用時(shí)卻是2.386s,超出了不少,這是為什么呢
在測(cè)試中加入b.Log(“NNNNN:”, b.N),再執(zhí)行基準(zhǔn)測(cè)試,并加入-v,打印測(cè)試中的日志
func BenchmarkSayHi(b *testing.B) {
for i := 0; i < b.N; i++ {
SayHi("Max")
}
b.Log("NNNNN:", b.N)
}
go test -v -bench=. -run=^$ gott/SayHi
BenchmarkSayHi
fun1_test.go:26: NNNNN: 1
fun1_test.go:26: NNNNN: 100
fun1_test.go:26: NNNNN: 10000
fun1_test.go:26: NNNNN: 1000000
fun1_test.go:26: NNNNN: 3541896
fun1_test.go:26: NNNNN: 4832275
BenchmarkSayHi-4 4832275 236.8 ns/op
PASS
ok gott/SayHi 2.395s
可以看到b.Log(“NNNNN:”, b.N)被執(zhí)行了6次,這證明了之前提到的,測(cè)試會(huì)對(duì)b.N依次遞增,直到執(zhí)行時(shí)間等于或超過(guò)上限。在對(duì)BenchmarkSayHi()運(yùn)行基準(zhǔn)測(cè)試時(shí),N值依次按1,100,10000,1000000,3541896,4832275遞增,直到執(zhí)行次數(shù)為4832275時(shí),執(zhí)行時(shí)間等于或超過(guò)了上限。
同時(shí)也說(shuō)明BenchmarkSayHi()一共被調(diào)用了6次,每次運(yùn)行BenchmarkSayHi()都要消耗一定的時(shí)間,所以測(cè)試總耗時(shí)為這6次調(diào)用時(shí)間之和,2.395s,超過(guò)了1秒
benchtime 標(biāo)記
可以通過(guò)-benchtime標(biāo)記修改默認(rèn)時(shí)間上限,比如改為3秒
go test -v -bench=. -benchtime=3s -run=^$ gott/SayHi
goos: darwin
goarch: amd64
pkg: gott/SayHi
BenchmarkSayHi
fun1_test.go:31: NNNNN: 1
fun1_test.go:32: /Users/ga/m/opt/go/go_root
fun1_test.go:31: NNNNN: 100
fun1_test.go:32: /Users/ga/m/opt/go/go_root
fun1_test.go:31: NNNNN: 10000
fun1_test.go:32: /Users/ga/m/opt/go/go_root
fun1_test.go:31: NNNNN: 1000000
fun1_test.go:32: /Users/ga/m/opt/go/go_root
fun1_test.go:31: NNNNN: 15927812
fun1_test.go:32: /Users/ga/m/opt/go/go_root
BenchmarkSayHi-4 15927812 223.4 ns/op
PASS
ok gott/hello 3.802s
還可以設(shè)置具體的探索次數(shù)最大值,格式為-benchtime=Nx
go test gott/hello -run=^$ -bench=BenchmarkHello -benchtime=50x
goos: darwin
goarch: amd64
pkg: gott/hello
BenchmarkHello-4 50 2183 ns/op
--- BENCH: BenchmarkHello-4
fun1_test.go:35: NNNNN: 1
fun1_test.go:36: /Users/ga/m/opt/go/go_root
fun1_test.go:35: NNNNN: 50
fun1_test.go:36: /Users/ga/m/opt/go/go_root
PASS
ok gott/hello 0.011s
b.N的值被設(shè)置為50,函數(shù)運(yùn)行了50次
benchmem 標(biāo)記
可以通過(guò)-benchmem標(biāo)記查看內(nèi)存使用信息
go test -bench=. -run=none -benchmem
go test gott/hello -run=^$ -bench=BenchmarkHello -benchmem
go test gott/hello -run=^$ -bench=BenchmarkHello -benchmem
goos: darwin
goarch: amd64
pkg: gott/hello
BenchmarkHello-4 5137456 223.1 ns/op 32 B/op 2 allocs/op
--- BENCH: BenchmarkHello-4
fun1_test.go:35: NNNNN: 1
fun1_test.go:36: /Users/ga/m/opt/go/go_root
fun1_test.go:35: NNNNN: 100
fun1_test.go:36: /Users/ga/m/opt/go/go_root
fun1_test.go:35: NNNNN: 10000
fun1_test.go:36: /Users/ga/m/opt/go/go_root
fun1_test.go:35: NNNNN: 1000000
fun1_test.go:36: /Users/ga/m/opt/go/go_root
fun1_test.go:35: NNNNN: 5137456
fun1_test.go:36: /Users/ga/m/opt/go/go_root
... [output truncated]
PASS
ok gott/hello 1.399s
- 32 B/op:平均每次迭代內(nèi)存分配的字節(jié)數(shù)
- 2 allocs/op:平均每次迭代內(nèi)存分配的次數(shù)
平均每次迭代計(jì)算的依據(jù)應(yīng)該使用的是 b.N=5137456迭代次數(shù)
基準(zhǔn)測(cè)試的用途
一般用于對(duì)比兩個(gè)不同的操作所消耗的時(shí)間,如
- 漸近增長(zhǎng)函數(shù)的運(yùn)行時(shí)間一個(gè)函數(shù)需要1ms處理1,000個(gè)元素,處理10000或1百萬(wàn)將需要多少時(shí)間呢
- I/O緩存該設(shè)置為多大基準(zhǔn)測(cè)試可以幫助我們選擇在性能達(dá)標(biāo)情況下所需的最小內(nèi)存
- 確定哪種算法更好
覆蓋率測(cè)試
運(yùn)行run with coverage
結(jié)果解讀
右側(cè)會(huì)展示覆蓋率,左側(cè)綠色為單元測(cè)試已覆蓋到的代碼,紅色為未覆蓋的代碼
example測(cè)試
樣例測(cè)試比較像平時(shí)在一些算法刷題平臺(tái)(比如LeetCode)的題目的一些例子,樣例測(cè)試以Example打頭,其邏輯也很簡(jiǎn)單,就是使用fmt.Println輸出該測(cè)試用例的返回結(jié)果,然后在函數(shù)體的末尾使用如圖的注釋?zhuān)灰粚?duì)應(yīng)每個(gè)fmt.Println的輸出:
如果輸出和注釋不能對(duì)應(yīng)上則不通過(guò)
模糊測(cè)試
go版本要求
Fuzz模糊測(cè)試需要Go 1.18 Beta 1或以上版本的泛型功能
測(cè)試代碼
package fuzz_test
import (
"fmt"
"testing"
"unicode/utf8"
)
func Reverse(s string) string {
b := []byte(s)
for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}
func FuzzReverse(f *testing.F) {
testcases := []string{"Hello, world", "!12345"}
for _, tc := range testcases {
f.Add(tc) // Use f.Add to provide a seed corpus
}
f.Fuzz(func(t *testing.T, orig string) {
rev := Reverse(orig)
fmt.Printf("original->:%s", orig)
fmt.Printf("after->:%s", rev)
doubleRev := Reverse(rev)
if orig != doubleRev {
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
}
測(cè)試結(jié)果
第三方框架
總體介紹
框架名 | 使用說(shuō)明 | 優(yōu)點(diǎn) | 缺點(diǎn) |
---|---|---|---|
testing | 如上 | go官方原生測(cè)試框架,簡(jiǎn)單好用 | 斷言不夠友好,需要大量if else可以配合testify的assert使用 |
testify | 1. 和 go test 無(wú)縫集成,直接使用該命令運(yùn)行2. 支持?jǐn)嘌?,?xiě)法更簡(jiǎn)便3. 支持 mock & suite功能 | mock的功能不夠強(qiáng)大,需要配合其他mock框架使用 | |
GoConvey | 1. 能夠使用 go test 來(lái)運(yùn)行測(cè)試2. 支持?jǐn)嘌裕瑢?xiě)法更簡(jiǎn)便3. 支持通過(guò)瀏覽器查看測(cè)試結(jié)果4. 支持嵌套,可以分組 | 1. 寫(xiě)法并不簡(jiǎn)便,主要多了個(gè)通過(guò)瀏覽器查看測(cè)試結(jié)果,個(gè)人覺(jué)得不是很有使用的必要2. 單元測(cè)試應(yīng)該盡可能簡(jiǎn)單可維護(hù),嵌套分組等功能太復(fù)雜,不利于維護(hù) | |
結(jié)論
建議采用testing+testify,goland支持自動(dòng)生成testing單測(cè)模板,加上testify豐富的斷言夠用了
mock框架
golang中常用的stub/mock框架
GoStub | Gomonkey | Gomock |
---|---|---|
輕量級(jí)打樁框架 | 運(yùn)行時(shí)重寫(xiě)可執(zhí)行文件,類(lèi)似熱補(bǔ)丁 | 官方提供的mock框架,功能強(qiáng)大 |
支持為全局變量,函數(shù)打樁 | 性能強(qiáng)大,使用方便 | mockgen 工具可自動(dòng)生成mock代碼;支持mock所有接口類(lèi)型 |
需要改造原函數(shù),使用不方便;性能不強(qiáng) | 支持對(duì)變量,函數(shù),方法打樁,支持打樁序列 | 可以配置調(diào)用次數(shù),調(diào)用順序,根據(jù)入?yún)?dòng)態(tài)返回結(jié)果等 |
不是并發(fā)安全的;使用可能根據(jù)版本不同需要有些額外配置工作 | 只支持接口級(jí)別mock,不能mock普通函數(shù) |
結(jié)論
建議采用Gomonkey. GoStub很多功能不支持,GoMock每次編寫(xiě)完需要重新generate生成代碼,不太方便
其他特定領(lǐng)域mock工具
框架名 | 說(shuō)明 | |
---|---|---|
GoSqlMock | sqlmock包,用于單測(cè)中mock db操作 | |
miniredis | 純go實(shí)現(xiàn)的用于單元測(cè)試的redis server。它是一個(gè)簡(jiǎn)單易用的、基于內(nèi)存的redis替代品,它具有真正的TCP接口。當(dāng)我們?yōu)橐恍┌?Redis 操作的代碼編寫(xiě)單元測(cè)試時(shí)可以使用它來(lái) mock Redis 操作 | |
Httptest | Golang官方自帶,生成一個(gè)模擬的http server.主要使用的單測(cè)場(chǎng)景是:已經(jīng)約定了接口,但是服務(wù)端還沒(méi)實(shí)現(xiàn) |
其他
goland中沒(méi)有類(lèi)似TestMe的Go單元測(cè)試插件,可以考慮實(shí)現(xiàn)一個(gè)
mock工具GoMock使用
GoMock
gomock 是官方提供的 mock 框架,同時(shí)還提供了 mockgen 工具用來(lái)輔助生成測(cè)試代碼。
go get -u github.com/golang/mock/gomock go get -u github.com/golang/mock/mockgen
簡(jiǎn)單的使用方法:
// db.go
type DB interface {
Get(key string) (int, error)
}
func GetFromDB(db DB, key string) int {
if value, err := db.Get(key); err == nil {
return value
}
return -1
}
復(fù)制代碼
有一個(gè)DB接口,使用mockgen產(chǎn)生一個(gè)mock對(duì)象
mockgen -source=db.go -destination=db_mock.go -package=main
下面是自動(dòng)生成的代碼
// Code generated by MockGen. DO NOT EDIT.
// Source: db.go
// Package mian is a generated GoMock package.
package mian
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockDB is a mock of DB interface.
type MockDB struct {
ctrl *gomock.Controller
recorder *MockDBMockRecorder
}
// MockDBMockRecorder is the mock recorder for MockDB.
type MockDBMockRecorder struct {
mock *MockDB
}
// NewMockDB creates a new mock instance.
func NewMockDB(ctrl *gomock.Controller) *MockDB {
mock := &MockDB{ctrl: ctrl}
mock.recorder = &MockDBMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockDB) EXPECT() *MockDBMockRecorder {
return m.recorder
}
// Get mocks base method.
func (m *MockDB) Get(key string) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", key)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get.
func (mr *MockDBMockRecorder) Get(key interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockDB)(nil).Get), key)
}
復(fù)制代碼
在測(cè)試的使用mock對(duì)象
func TestGetFromDB(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish() // 斷言 DB.Get() 方法是否被調(diào)用
m := NewMockDB(ctrl)
m.EXPECT().Get(gomock.Eq("Tom")).Return(100, errors.New("not exist")) //設(shè)置期望返回結(jié)果,可以設(shè)置可調(diào)用次數(shù)times/AnyTimes
if v := GetFromDB(m, "Tom"); v != -1 {
t.Fatal("expected -1, but got", v)
}
}
復(fù)制代碼
goMock支持對(duì)特定輸入打樁和對(duì)任意輸入打樁(gomock.any()),可根據(jù)具體情況使用;文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-491542.html
實(shí)際項(xiàng)目中,可以用gomock來(lái)mock dao層和rpc層代碼,隔離外部依賴文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-491542.html
到了這里,關(guān)于Go單元測(cè)試及框架使用的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!