go語言內置的測試框架能夠完成基本的功能測試,基準測試,和樣本測試。
測試框架
- go語言測試單元以包為單位組織,包含包里的一個或者多個測試文件。
- 測試文件以_test.go結尾,通常放在待測代碼相同目錄下,即他們屬于同一個包。
- 測試用例以TestXxxx/ExampleXxxx/BenchmarkXxxx的格式組織在測試文件里。
當然也可以在測試文件里面定義其他非上述格式的本地函數,以供調用。
功能測試: TestXxxx(t *testing.T)
功能測試偏向于測試源碼文件中的具體功能,屬于UT的范疇,比如具體的函數功能。
先舉一個例子來說
假定定義了一個函數計算平均值:
$ cat myavg.go
package myavg
func MyAvg(a []float64) float64 {
...
}
下面來為這個函數增加測試用例
$ cat myavg_test.go
package myavg
import (
"testing"
)
func TestMyAvg(t *testing.T) {
a := []float64 {1, 2, 3, 4, 5}
e := 3
v := MyAvg(a)
if v != 3 {
t.Errorf("case (%v) expect (%v) but got (%v)", a, e, v)
}
}
運行
$ go test -v
=== RUN TestMyAvg
--- PASS: TestMyAvg (0.00s)
PASS
ok myavg 0.008s
幾點說明:
- 測試文件是否與待測文件在相同目錄下
這并不是固定的;如果放在同一個目錄下使用相同的包(package)名,方便當代碼功能更改時,及時相應的更改測試代碼;如果把測試文件單獨組織在另一個包下面,例如myavg_test,也可以完成測試功能,例如:
$ cat myavg_test.go
package myavg_test
import (
"myavg"
"testing"
)
func TestMyAvg(t *testing.T) {
a := []float64 {1, 2, 3, 4, 5}
e := 3
v := myavg.MyAvg(a)
if v != 3 {
t.Errorf("case (%v) expect (%v) but got (%v)", a, e, v)
}
}
這個例子中我們把測試文件放在一個單獨的包(myavg_test)里,區別就是必須import myavg包,然后調用MyAvg()的時候必須指定包名myavg,因為他們屬于不同的包(package)了。
其實go只規定了一點,即以_test.go結尾的文件會認為是測試文件,在編譯(go build)的時候會被忽略,只在測試(go test)的時候被使用,所以測試文件是否和待測文件放在相同包里還是不同包里,可以根據項目,或者使用習慣組織。
- 測試用例名稱TestXxxx
注意第一個字母X不能是小寫字母,在實際項目中,第一個字母一般是大寫字母或者下劃線(_),這樣便于代碼的閱讀;其實也可以是數字,但這實在是可讀性差點了,雖然語法上沒有問題。例如
TestMyAvg // OK
Test_MyAvg // OK
Test_myAvg // OK
Test1myAvg // not suggested
TestmyAvg // Fail
測試例子的改進
前面的測試例子TestMyAvg中,我們給了一個用例,這個例子我們可以進行適當改進,方便多個組合的測試。
package myavg
import (
"testing"
)
type casepair struct {
val []float64
avg float64
}
func TestAverage2(t *testing.T) {
var cases = []casepair {
{ []float64{1, 2}, 1.5 },
{ []float64{1, 1, 1, 1, 1, 1}, 1 },
{ []float64{1, -1}, 0 },
}
for _, pair := range cases {
v := MyAvg(pair.val)
if v != pair.avg {
t.Errorf("case (%v) expect (%v) but got (%v)", pair.val, pair.avg, v)
}
}
}
運行
$ go test -v
=== RUN TestAverage2
--- PASS: TestAverage2 (0.00s)
PASS
ok myavg 0.003s
以這種方式組織測試材料,在定義輸入的時候就定義好了結果輸出,便于測試用例的組織,以及將來的添加,刪除,等修改維護。
樣本測試: ExampleXxxx()
樣本測試用來驗證運行的輸出內容是否與預期的一樣。
樣本測試的格式和功能測試的格式類似,只是例子以ExampleXxxx格式,然后在函數中以//Output的方式指明輸出,例如
package myavg
import (
"fmt"
)
func ExampleHello() {
a := []float64 {1, 2, 3, 4, 5}
v := MyAvg(a)
fmt.Printf("%.1f", v)
// Output: 3.0
}
type casepair struct {
val []float64
avg float64
}
func ExampleHello2() {
var cases = []casepair {
{ []float64{1, 2}, 1.5 },
{ []float64{1, 1, 1, 1, 1, 1}, 1 },
{ []float64{1, -1}, 0 },
}
for _, pair := range cases {
v := MyAvg(pair.val)
fmt.Printf("%.1f\n", v)
}
// Output:
// 1.5
// 1.0
// 0.0
}
寫在注釋后面的Output定義了函數執行希望的輸出內容,go測試框架會比較這些輸出和實際的輸出是否一致,來決定測試用例是否通過。另外樣本測試并不需要import testing包,而只需要定義函數名格式為ExampleXxxx即可,testing包是功能測試必須的。
定義Output的格式比較復雜可以參考go語言文檔,這里不細說了,例子只給出了最基本的Output用法。
其實仔細想想,功能測試和樣本測試沒啥區別,他們可以互相改造,只是內容比較,用戶可以以程序的方式比較字符串內容,或者由測試框架來比較而已。所以實際場景下樣本測試使用的并不多。
運行:
$ go test -v
=== RUN ExampleHello
--- PASS: ExampleHello (0.00s)
=== RUN ExampleHello2
--- PASS: ExampleHello2 (0.00s)
PASS
ok myavg 0.003s
我們給一個沒有通過反例看一下失敗的場景是怎么樣的:
package myavg
import (
"fmt"
)
func ExampleHello3() {
fmt.Println("Hello")
// Output:
// hello
}
$ go test -v
=== RUN ExampleHello
--- FAIL: ExampleHello3 (0.00s)
got:
Hello
want:
hello
FAIL
exit status 1
FAIL myavg 0.004s
測試用例里面希望的輸出是hello,而實際的執行輸出是Hello,第一個字母的大小寫不一致,所有這個用例運行失敗。
基準測試: BenchmarkXxxx(b *testing.B)
沒有弄過,以后再更新。
測試Main函數: TestMain(m *testing.M)
測試的TestMain函數主要用來在運行測試用例之前做一些必要的setup以及之后的tearDown操作。注意是在所有的測試運行之前和之后,因此一個測試包里面只能定義一個TestMain函數,下面的例子:
package myavg
import (
"os"
"log"
"flag"
"testing"
)
var maxValue *int = flag.Int ("max", 100, "the maxinum buffer size")
var typeValue *string = flag.String("type", "average", "the default type value")
func TestMain(m *testing.M) {
flag.Parse()
log.Printf("option[max]=(%d)\n", *maxValue)
log.Printf("option[type]=(%s)\n", *typeValue)
setup(*maxValue, *typeValue)
exitcode := m.Run() // run all cases
tearDown()
os.Exit(exitcode)
}
func setup(maxValue int, typeValue string) {
log.Println("Entry of setup")
}
func tearDown() {
log.Println("Exit of tearDown")
}
運行
$ go test -v -max 12 -type anytype
2017/11/03 21:59:57 option[max]=(12)
2017/11/03 21:59:57 option[type]=(anytype)
2017/11/03 21:59:57 Entry of setup
...
2017/11/03 21:59:57 Exit of tearDown
ok myavg 0.004s
首先在TestMain分析命令行參數,我們定義了max和type兩個參數,然后把參數傳個setup()做必要的初始化,接著運行測試案例,最后退出之前調用tearDown()做必要的清除操作。