有关测试的作用以及必要性,这篇文章就不多说了。
go
语言中进行测试是很方便的,官方提供了比较完善的测试方式,使用 go test
命令和 testing
标准库。
目录结构如下图
源文件和测试文件放在同一目录下,测试文件以 _test
结尾,这个是固定格式,使用 go build
进行编译时,_test
文件不会编译。
测试函数也有讲究:
Test
为前缀,例如:Test_Division
, TestDivision
Benchmark
为前缀,例如:Benchmark_Division
, BenchmarkDivision
由于篇幅问题,源代码放上面比较占位置,顾放最后。
-v
go test -v
-run
go test -run regexp
-bench
go test -bench regexp
go test -bench .
"."表示执行包下所有的性能测试函数
-benchmem
go test -bench="." -benchmem
“16 B/op”表示每一次调用需要分配 16 个字节,“1 allocs/op”表示每一次调用有一次分配
-benchtime
go test -v -bench=. -benchtime=5s
基准测试框架的默认测试时间为1s,可以通过 -benchtime
参数来指定测试时间。
-cover
go test -cover
举个例子:net/url
包,提供了URL解析的功能;net/http
包,提供了 web
服务和 HTTP
客户端的功能。如我们所料,上层的 net/http
包依赖下层的 net/url
包。然后,net/url
包中的一个测试是演示不同 URL
和 HTTP
客户端的交互行为。
也就是说,一个下层包的测试代码导入了上层的包。这样的行为在 net/url
包的测试代码中会导致包的循环依赖,go
语言规范是禁止包的循环依赖的。
此时可以在 net/url
包所在目录声明一个独立的 url_test
测试包(_test
为固定结尾格式,用于告诉 go test
工具它应该建立一个额外的包来运行测试),这样就可以避免循环引用的问题了。
这个也是一个比较特殊的目录,go build
编译时,会自动忽略 testdata
目录,并且在运行 go test
指令时,会将 test
文件所在目录设置为根目录,可以直接使用相对路径 testdata
引入或者存储相关文件。
简而言之,testdata
目录的使用场景,就是能够很直观的通过文件内容对比,发现测试结果是否符合预期,适用于输入输出都比较复杂的情况。
go
官方标准库 cmd/gofmt/gofmt_test.go
源码中就有用到,可参考。
go test -tags="tagName"
使用的比较少。
这个规则常常用在集成测试上,或者结合版本控制来使用。
Skip
方法)例:根据环境变量来测试
// 使用环境变量的方式去进行测试
func TestDivison(t *testing.T) {
divAddr := os.Getenv("DIV_ADDR")
if divAddr == "" {
t.Skip("set DIV_ADDR to run this test") //Skip方法会跳过当前测试
}
t.Log("do DIV_ADDR test")
}
通过在测试函数内使用 t.Parallel()
来标志当前测试函数为并行测试模式,t.Parallel()
会重置测试时间,通常在测试函数体中第一个被调用。并行的数量受 GOMAXPROCS
变量影响,也可以通过 go test -parallel n
的方式来指定并行测试的数量。
并行测试的性能有待测试,在测试规则比较复杂,测试时间比较长的情况下,可能效果会比较明显,对于普通的测试函数,有可能会导致效率下降。
func TestParallel(t *testing.T) {
t.Parallel()
// actual test...
}
go
官方标准库里使用最多的测试范例
func TestSplit(t *testing.T) {
//官方标准库喜欢把变量写在Test函数体外面,更有助于代码阅读和修改
//声明一个结构体的map,并且用string作为key区分不同的测试案例,struct结构体内包含了用于测试用的相关字段,字段可以自由定义。
tests := map[string]struct {
input string
sep string
want []string
}{
//采用map结构,可以很方便的添加或者删除测试用例
"simple": {input: "a/b/c", sep: "/", want: []string{"a", "b", "c"}},
"wrong sep": {input: "a/b/c", sep: ",", want: []string{"a/b/c"}},
"no sep": {input: "abc", sep: "/", want: []string{"abc"}},
"trailing sep": {input: "a/b/c/", sep: "/", want: []string{"a", "b", "c"}},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) { //name这里很关键,不然只知道出错,但是不知道具体是上面4个测试用例中哪一个用例出错。
got := strings.Split(tc.input, tc.sep)
if !reflect.DeepEqual(tc.want, got) {
t.Fatalf("expected: %v, got: %v", tc.want, got)
//t.Fail() // 只标记错误,不终止测试
//t.FailNow() // 标记错误并终止
}
})
}
}
源码在 gitee tTesting 包中,该项目还包含了一些本人日常测试使用的一些代码,感兴趣的可以看下。
package tTesting
import "errors"
// 参考资料
// http://c.biancheng.net/view/124.html
// https://mp.weixin.qq.com/s/HzET8y7lRa7NzJhB49ATtg
func Division(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("除数不能为0")
}
return a / b, nil
}
// 这个变量是用来测试 go test
var a string = "a"
package tTesting
import (
"os"
"reflect"
"strings"
"testing"
)
// 这里也是可以使用init的
func init() {
// os.Setenv("FOO_ADDR", "foo")
}
//gotest_test.go:这是我们的单元测试文件,但是记住下面的这些原则:
//
//文件名必须是_test.go结尾的(文件名必须是*_test.go的类型,*代表要测试的文件名),这样在执行go test的时候才会执行到相应的代码
//你必须import testing这个包
//所有的测试用例函数必须是Test开头(函数名必须以Test开头如:TestXxx或Test_xxx)
//测试用例会按照源代码中写的顺序依次执行
//测试函数TestXxx()的参数是testing.T,我们可以使用该类型来记录错误或者是测试状态
//测试格式:func TestXxx (t *testing.T),Xxx部分可以为任意的字母数字的组合,但是首字母不能是小写字母[a-z],例如Testintdiv是错误的函数名。
//函数中通过调用testing.T的Error, Errorf, FailNow, Fatal, FatalIf方法,说明测试不通过,调用Log方法用来记录测试的信息。
// 在go build指令打包时,会自动忽略testdata目录里面的内容,并且在运行go test指令时,会将test文件所在目录设置为根目录,
// 可以直接使用相对路径testdata引入或者存储相关文件,这是一个很有用的特性。通常我们会把一些较大的文件,如json文件,txt等文本文件存储在testdata目录下面,或者下面提到的.golden文件。
func Test_Division_1(t *testing.T) {
if i, e := Division(6, 2); i != 3 || e != nil { //try a unit test on function
t.Error("除法函数测试没通过") // 如果不是如预期的那么就报错
} else {
t.Log("第一个测试通过了") //记录一些你期望记录的信息
}
}
func Test_Division_2(t *testing.T) {
if _, e := Division(6, 1); e == nil { //try a unit test on function
t.Log("Division did not work as expected.") // 如果不是如预期的那么就报错
} else {
t.Error("one test passed.", e) //记录一些你期望记录的信息
}
}
func TestVar(t *testing.T) {
t.Log(a)
}
// 使用环境变量的方式去进行测试
func TestIntegration(t *testing.T) {
fooAddr := os.Getenv("FOO_ADDR")
if fooAddr == "" {
t.Skip("set FOO_ADDR to run this test") //Skip方法会跳过当前测试
}
t.Log("do FOO_ADDR test")
//f, err := foo.Connect(fooAddr)
// ...
}
// 表驱动测试(Table Driven Test)
func TestSplit(t *testing.T) {
//官方标准库喜欢把变量写在Test函数体外面,更有助于代码阅读和修改
//声明一个结构体的map,并且用string作为key区分不同的测试案例,struct结构体内包含了用于测试用的相关字段,字段可以自由定义。
tests := map[string]struct {
input string
sep string
want []string
}{
//采用map结构,可以很方便的添加或者删除测试用例
"simple": {input: "a/b/c", sep: "/", want: []string{"a", "b", "c"}},
"wrong sep": {input: "a/b/c", sep: ",", want: []string{"a/b/c"}},
"no sep": {input: "abc", sep: "/", want: []string{"abc"}},
//"trailing sep": {input: "a/b/c/", sep: "/", want: []string{"a", "b", "c"}},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) { //name这里很关键,不然只知道出错,但是不知道具体是上面4个测试用例中哪一个用例出错。
got := strings.Split(tc.input, tc.sep)
if !reflect.DeepEqual(tc.want, got) {
//t.Fatalf("expected: %v, got: %v", tc.want, got)
t.Fail() // 只标记错误,不终止测试
//t.FailNow() // 标记错误并终止
}
})
}
}
package tTesting
import (
"fmt"
"testing"
)
func Benchmark_Division(b *testing.B) {
for i := 0; i < b.N; i++ { //use b.N for looping
Division(4, 5)
}
}
func Benchmark_TimeConsumingFunction(b *testing.B) {
b.StopTimer() //调用该函数停止压力测试的时间计数
//做一些初始化的工作,例如读取文件数据,数据库连接之类的,
//这样这些时间不影响我们测试函数本身的性能
b.StartTimer() //重新开始时间
for i := 0; i < b.N; i++ {
Division(4, 5)
}
}
// 内存测试
// go test -bench="." -benchmem
func Benchmark_Alloc(b *testing.B) {
for i := 0; i < b.N; i++ {
fmt.Sprintf("%d", i)
}
}
go语言圣经-测试
http://c.biancheng.net/view/124.html
https://mp.weixin.qq.com/s/HzET8y7lRa7NzJhB49ATtg