go语言测试框架
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()做必要的清除操作。