
Go 語言中自帶了測試框架,在不引入外部包的情況下,也可以編寫完整的測試。這篇文章來看一下Go 提供原生測試能力,及其不足之處,以及補(bǔ)充這些不足的方法。
1. 基本測試框架
在 Go 語言中,所有的測試都需要以 _test.go 結(jié)尾,這樣go build 不會去編譯 _test.go 結(jié)尾的文件,而 go test 會去編譯 _test.go 結(jié)尾的文件。
在編寫測試的時(shí)候,我們都會用到 testing 這個(gè)包,在這個(gè)包中,常用的類型有下面這些:
- testing.T
- testing.B
- testing.M
testing.TB 和 testing.PB 平時(shí)用的不多,在這里就不展開說,感興趣的可以自行去搜索。上面的三類代表了三種不同的測試,分別是單元測試、基準(zhǔn)測試和 TestMain 測試,對于不同的測試,在測試方法的入?yún)⒅?,必須帶上這個(gè)類型。
有一類測試?yán)?,那就?Example 測試,這個(gè)測試主要用來在文檔中輸出一些測試案例,Example 測試必須以 Example 開頭,方法不需要任何參數(shù),同時(shí)要指明這個(gè)實(shí)例的輸出,像下面這樣:
func ExampleTest() {
fmt.Println("run example test")
// Output:
// run example test
}
所有的測試都可以通過 go test 來發(fā)起,例如,在當(dāng)前包下發(fā)起測試 go test -v ./ ,-v 參數(shù)表示打印測試的過程,會把測試過程中的標(biāo)準(zhǔn)輸出都打印出來。
2. 單元測試
單元測試的編寫需要按照一定的規(guī)則來,所有的單元測試都需要以 Test 開頭,后面加上測試的方法名稱,就像下面這樣:
import (
"fmt"
"testing"
)
func TestDemo(t *testing.T) {
fmt.Println(" run test demo")
}
這就是一個(gè)最簡單的單元測試,在實(shí)際使用中,一組測試可能會有多個(gè)單元測試,而且要同時(shí)運(yùn)行,這時(shí)我們就需要一個(gè)方法將這些測試串聯(lián)起來,那就需要用到 TestMain 了,這個(gè)方法名稱和簽名是統(tǒng)一的,只能是下面的寫法:
func TestMain(m *testing.M) {
fmt.Println("begin test")
m.Run()
fmt.Println("end test")
}
TestMain(m *testing.M) 在一個(gè)包下只能有一個(gè),測試執(zhí)行的時(shí)候,會先執(zhí)行這個(gè)方法,然后再去執(zhí)行這個(gè)包下的所有 Test 測試和 Example 測試,基準(zhǔn)測試則不會執(zhí)行。
而且這個(gè)方法可以用來初始化和回收資源,有些測試在運(yùn)行之前需要初始化一些配置,連接數(shù)據(jù)庫、釋放數(shù)據(jù)庫連接等操作,就可以在這個(gè)測試中完成。
寫完上面的測試之后,就可以運(yùn)行測試了,這樣會從 TestMain 開始,運(yùn)行所有的 Test 和 Example 測試:
$ go test -v ./
但有時(shí)候我們也會關(guān)心單元測試的覆蓋率,只要加上一個(gè)參數(shù)就可以看到測試的覆蓋率:
$ go test -cover -v ./
3. 基準(zhǔn)測試
基準(zhǔn)測試通常用來測試某個(gè)程序的性能,基準(zhǔn)測試必須要用 Benchmark 開頭,同時(shí)方法的入?yún)⒈仨毷?testing.B,就像下面這樣:
func BenchmarkDemo(b *testing.B) {
fmt.Println("run benchmark demo")
}
為了更好的說明基準(zhǔn)測試的功能,我用之前測試字符串拼接的基準(zhǔn)測試的例子來說明:
func BenchmarkPlus(b *testing.B) {
str := "this is just a string"
for i := 0; i < b.N; i++ {
stringPlus(str)
}
}
func stringPlus(str string) string {
s := ""
for i := 0; i < 10000; i++ {
s += str
}
return s
}
其中 b.N 不是一個(gè)固定的值,這個(gè)值的大小由框架自己來決定,上面這側(cè)測試的內(nèi)容是對于一個(gè)要拼接一萬次字符傳的函數(shù)進(jìn)行性能測試,至于這個(gè)測試運(yùn)行多少次,由框架自己決定。
運(yùn)行基準(zhǔn)測試的命令如下:
$ go test -bench=. -benchmem .
輸出結(jié)果如下:
goos: darwin
goarch: amd64
pkg: zxin.com/zx-demo/string_benchmark
**BenchmarkPlus-12 12 96586447 ns/op 1086401355 B/op 10057 allocs/op**
PASS
ok zxin.com/zx-demo/string_benchmark 6.186s
加粗的那行是基準(zhǔn)測試的輸出,每列信息的具體含義如下:
- 第一列表示基準(zhǔn)測試的方法名稱和所用的 GOMAXPROCS 的值
- 第二列表示這次測試循環(huán)的次數(shù)
- 第三列表示平均每次測試所用的時(shí)間,單位為納秒
- 第四列表示平均每次運(yùn)行所分配的內(nèi)存
- 第五列表示每次運(yùn)行所分配內(nèi)存的次數(shù)
4. 測試加強(qiáng)
但原生的測試包不夠完美,比如在單元測試中,就缺少斷言機(jī)制,使得在判斷測試結(jié)果的時(shí)候,非常不方便,有一個(gè)外部的包可以幫助完善測試的功能。
安裝也很方便:
$ go get github.com/stretchr/testify
這個(gè)包從三個(gè)方面擴(kuò)展了 Go 原生測試框架的能力:
- 斷言
原生測試框架里面缺失斷言功能,在很多場景下都不方便,testify 提供的斷言功能開箱即用,與原生測試框架完美契合:
func TestAssert(t *testing.T) {
assert := assert.New(t)
assert.Equal(123, 123, "they should be equal")
assert.NotEqual(123, 456, "they should not be equal")
o := make(map[string]string)
o["ray"] = "jun"
if assert.NotNil(o) {
assert.Equal("jun", o["ray"])
} else {
assert.Nil(o)
}
}
- Mock 能力
testify 提供了Mock 的能力,可以很好的模擬測試需要的數(shù)據(jù),對于一些需要復(fù)雜數(shù)據(jù)的測試很有幫助:
type MyMockedObject struct{
mock.Mock
}
func (m *MyMockedObject) DoSomething(number int) (bool, error) {
args := m.Called(number)
return args.Bool(0), args.Error(1)
}
func TestSomething(t *testing.T) {
testObj := new(MyMockedObject)
testObj.On("DoSomething", 123).Return(true, nil)
testMockObj(testObj)
testObj.AssertExpectations(t)
}
func testMockObj(mcObj *MyMockedObject) {
fmt.Println(mcObj.DoSomething(123))
}
- 構(gòu)建更完善的測試
即使有了 TestMain 來初始化配置,但也還是不夠靈活,比如在一個(gè)包下,我需要包含多組測試,而且每組測試的初始化都不一樣,而 testify 提供的 suite 包提供了更加面向?qū)ο蟮臏y試方式,并且也提供了 setup/teardown 等方法來初始化和回收資源,可以直接使用 go test 進(jìn)行測試,不會對現(xiàn)有的測試框架有侵入性修改:
type ExampleTestSuite struct {
suite.Suite
VariableThatShouldStartAtFive int
}
func (suite *ExampleTestSuite) SetupTest() {
fmt.Println("run setup method")
suite.VariableThatShouldStartAtFive = 5
}
func (suite *ExampleTestSuite) TearDownTest() {
fmt.Println("run tear down method")
suite.VariableThatShouldStartAtFive = 0
}
func (suite *ExampleTestSuite) TestExample() {
assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive)
}
func TestExampleTestSuite(t *testing.T) {
suite.Run(t, new(ExampleTestSuite))
}
5. 小結(jié)
雖然 Go 原生測試框架已經(jīng)支持編寫很復(fù)雜的測試,但很多場景下還不是很方便,這時(shí)候就有必要引入新的測試加強(qiáng)包 testify,這個(gè)包基本做到了開箱即用,而且不會破壞現(xiàn)有的測試流程。
文 / Rayjun