go test 的使用(指定执行某个test, benchmark, testdata, cover, benchmem......)

简介

有关测试的作用以及必要性,这篇文章就不多说了。
go 语言中进行测试是很方便的,官方提供了比较完善的测试方式,使用 go test 命令和 testing 标准库。

基础用法

目录结构如下图目录
源文件和测试文件放在同一目录下,测试文件以 _test 结尾,这个是固定格式,使用 go build 进行编译时,_test 文件不会编译。
测试函数也有讲究:

  1. 每个测试函数需要以 Test 为前缀,例如:Test_Division, TestDivision
  2. 每个性能测试函数需要以 Benchmark 为前缀,例如:Benchmark_Division, BenchmarkDivision

由于篇幅问题,源代码放上面比较占位置,顾放最后。

go test 参数

打印测试函数的所有细节 -v

go test -v

go test 的使用(指定执行某个test, benchmark, testdata, cover, benchmem......)_第1张图片

指定运行某一测试函数 -run

go test -run regexp

只运行 regexp 匹配的函数
例:在这里插入图片描述

性能测试 -bench

go test -bench regexp

go test -bench . "."表示执行包下所有的性能测试函数
go test 的使用(指定执行某个test, benchmark, testdata, cover, benchmem......)_第2张图片

内存测试 -benchmem

go test -bench="." -benchmem

go test 的使用(指定执行某个test, benchmark, testdata, cover, benchmem......)_第3张图片
“16 B/op”表示每一次调用需要分配 16 个字节,“1 allocs/op”表示每一次调用有一次分配

自定义测试时间 -benchtime

go test -v -bench=. -benchtime=5s

基准测试框架的默认测试时间为1s,可以通过 -benchtime 参数来指定测试时间。
go test 的使用(指定执行某个test, benchmark, testdata, cover, benchmem......)_第4张图片

开启覆盖测试 -cover

go test -cover

在这里插入图片描述

当存在循环引用问题时,创建测试包 xxx_test 来进行测试

举个例子:net/url 包,提供了URL解析的功能;net/http 包,提供了 web 服务和 HTTP 客户端的功能。如我们所料,上层的 net/http 包依赖下层的 net/url 包。然后,net/url 包中的一个测试是演示不同 URLHTTP 客户端的交互行为。

也就是说,一个下层包的测试代码导入了上层的包。这样的行为在 net/url 包的测试代码中会导致包的循环依赖,go 语言规范是禁止包的循环依赖的。

此时可以在 net/url 包所在目录声明一个独立的 url_test 测试包(_test 为固定结尾格式,用于告诉 go test 工具它应该建立一个额外的包来运行测试),这样就可以避免循环引用的问题了。

testdata 目录

这个也是一个比较特殊的目录,go build 编译时,会自动忽略 testdata 目录,并且在运行 go test 指令时,会将 test 文件所在目录设置为根目录,可以直接使用相对路径 testdata 引入或者存储相关文件。

简而言之,testdata 目录的使用场景,就是能够很直观的通过文件内容对比,发现测试结果是否符合预期,适用于输入输出都比较复杂的情况。

go 官方标准库 cmd/gofmt/gofmt_test.go 源码中就有用到,可参考。

使用 tag 结合做测试

go test -tags="tagName"

使用的比较少。
这个规则常常用在集成测试上,或者结合版本控制来使用。
go test 的使用(指定执行某个test, benchmark, testdata, cover, benchmem......)_第5张图片

跳过测试(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")
}

多线程测试 Parallel

通过在测试函数内使用 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() // 标记错误并终止
			}
		})
	}
}

go test 的使用(指定执行某个test, benchmark, testdata, cover, benchmem......)_第6张图片

本篇测试源码

源码在 gitee tTesting 包中,该项目还包含了一些本人日常测试使用的一些代码,感兴趣的可以看下。

hello.go
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"
hello_test.go
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() // 标记错误并终止
			}
		})
	}
}
hello_b_test.go
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

你可能感兴趣的:(go,go,go,test)