Go语言测试:testing
学习参考来源:https://www.liwenzhou.com/posts/Go/16_test/
go test工具
必须导入包:
import "testing"
go test命令是一个按照一定约定和组织的测试代码的驱动程序,所有以_test.go
为后缀名的源代码文件都是go test
测试的一部分,不会被go build
编译到最终的可执行文件中。
在*_test.go
文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。
类型 | 格式 | 作用 |
---|---|---|
测试函数 | 函数名前缀为Test | 测试程序的一些逻辑行为是否正确 |
基准函数 | 函数名前缀为Benchmark | 测试函数的性能 |
示例函数 | 函数名前缀为Example | 为文档提供示例文档 |
go test
命令会遍历所有的*_test.go
文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。
go test命令常用参数
go test
go test -v ./...
文件夹里还包括文件夹,可以添加./...
来递归测试。go test -v
添加-v
参数,查看测试函数名称和运行时间;go test -v ./split_test.go
指定运行某个测试文件;- 测试指定的_test.go文件,如果是在同一个包下,需要把测试文件和源文件都写出来;
go test -v ./split.go ./split_test.go
go test -v ./split.go ./split_test.go -test.run TestSplit
测试split_test.go里的某一个方法;
不同环境下编译go test可执行文件,编译以后可以直接使用e2e.test,
GOOS=darwin go test -c ./e2e -o e2e.test
GOOS=linux go test -c ./e2e -o e2e.test
GOOS=windows go test -c ./e2e -o e2e.test
测试函数
基本使用
在文件名为split_test.go
,格式固定为*_test.go
package split
import (
"fmt"
"reflect"
"testing"
)
func TestSplit(t *testing.T) { // 格式固定为:Test*(){}
got := Split("a:b:c", ":")
except := []string{"a", "b", "c"}
if !reflect.DeepEqual(got, except) { // 因为slice不能比较直接,借助反射包中的方法比较
fmt.Printf("excepted:%v, got:%v", except, got) // 测试失败输出错误提示
}
}
其中参数t
用于报告测试失败和附加的日志信息。 testing.T
的拥有的方法如下:
这里只介绍集中常用到的,链接出是全部说明:https://studygolang.com/static/pkgdoc/pkg/testing.htm#T
-
func (c *T) Error(args ...interface{}) 相当于在调用 Log 之后调用 Fail;
-
func (c *T) Errorf(format string, args ...interface{})
-
func (c *T) Fail()
将当前测试标识为失败,但是仍继续执行该测试; -
func (c *T) FailNow()
将当前测试标识为失败并停止执行该测试,在此之后,测试过程将在下一个测试或者下一个基准测试中继续;FailNow 必须在运行测试函数或者基准测试函数的 goroutine 中调用,而不能在测试期间创建的 goroutine 中调用。调用 FailNow 不会导致其他 goroutine 停止。
-
func (c *T) Fatal(args ...interface{})
调用 Fatal 相当于在调用 Log 之后调用 FailNow; -
func (c *T) Fatalf(format string, args ...interface{})
-
func (c *T) Log(args ...interface{})
-
func (c *T) Logf(format string, args ...interface{})
测试组和子测试
func TestGroupSub(t *testing.T) {
type test struct { // 定义test结构体
input string
sep string
want []string
}
tests := map[string]test{ // 测试用例使用map存储,测试组(逻辑上)
"simple": {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
"wrong sep": {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
"more sep": {input: "abcd", sep: "bc", want: []string{"a", "d"}},
"leading sep": {input: "我爱你", sep: "爱", want: []string{"我", "你"}},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) { //子测试
got := Split(tc.input, tc.sep)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("name:%s excepted:%#v, got:%#v", name, tc.want, got) // 将测试用例的name格式化输出
}
})
}
}
output:
--- PASS: TestGroupSub (0.00s)
--- PASS: TestGroupSub/simple (0.00s)
--- PASS: TestGroupSub/wrong_sep (0.00s)
--- PASS: TestGroupSub/more_sep (0.00s)
--- PASS: TestGroupSub/leading_sep (0.00s)
PASS
测试覆盖率
暂时不常用到,了解:https://www.liwenzhou.com/posts/Go/16_test/#autoid-2-4-0
基准测试
https://www.liwenzhou.com/posts/Go/16_test/#autoid-2-5-0
https://studygolang.com/static/pkgdoc/pkg/testing.htm#B
func BenchmarkName(b *testing.B){
// ...
}
func BenchmarkSplit(b *testing.B) {
for i := 0; i < b.N; i++ {
got := Split("a:b:c", ":")
except := []string{"a", "b", "c"}
if !reflect.DeepEqual(got, except) {
fmt.Printf("excepted:%v, got:%v", except, got)
}
}
}
$ go test -bench=Split -benchmem
goos: darwin
goarch: amd64
pkg: golearning/1125/gotest_demo/split
BenchmarkSplit-8 1489494 794 ns/op 320 B/op 12 allocs/op
PASS
ok golearning/1125/gotest_demo/split 3.003s
根据输出信息:
- 基准测试并不会默认执行,需要增加
-bench
参数; - 基准测试添加
-benchmem
参数,来获得内存分配的统计数据; - 其中
BenchmarkSplit-8
表示对Split函数进行基准测试,数字8
表示GOMAXPROCS
的值,这个对于并发基准测试很重要。1489494
和794ns/op
表示平均每次调用Split
函数耗时794ns`; 320 B/op
表示每次操作内存分配了320字节,12 allocs/op
则表示每次操作进行了12次内存分配。- 在执行中会根据实际的case执行时间是否是稳定的,会一直增加b.N的次数以达到执行时间是一种稳定的状态.比如说第一次我们执行10nm,第二次100nm,第三次300nm,那他就不会停止,会一直增加,等到平均每次执行的时间趋近于稳定他才会停;
内存分配次数非常占用时间,所以需要进行优化,减少内存分配:
func Split(s, sep string) (result []string) {
result = make([]string, 0, strings.Count(s, sep)+1) // strings.Count(s, sep)how many sep in s
i := strings.Index(s, sep) // 找到sep所在的下标
for i > -1 { // strings.Index(s, sep), 当s中没有sep时,return -1;
result = append(result, s[:i]) //
s = s[i+len(sep):] // 如果是中文的话就不是+1,要加上一个中文字符的长度;
i = strings.Index(s, sep) //
}
result = append(result, s)
return
}
goos: darwin
goarch: amd64
pkg: golearning/1125/gotest_demo/split
BenchmarkSplit-8 1894738 641 ns/op 256 B/op 10 allocs/op
PASS
ok golearning/1125/gotest_demo/split 2.581s
TestMain
一个包下面只能有一个TestMain方法。这个方法就和main()方法差不太多。他会在其他的Test方法之前执行,我们可以利用他做一些初始化数据操作,执行完后释放资源等操作。;
在*_test.go
文件中定义TestMain
函数;
func TestMain(m *testing.M) {
if os.Getenv("TEST_ENV") == "" {
fmt.Println("env is not configured")
return
}
setup() //自定义的 setup something
code := m.Run() //运行测试的主程序
tearDown() //自定义的,有时候测试额外添加了一些东西,通过这个让测试前后几乎无影响;
os.Exit(code)
}
一旦使用了TestMain(m *testing.M)
,不管怎么测试都是通过这个类似main函数顺序执行的;也就是先setup,m.Run()执行测试,tearDown()取消设置;
就算是只运行某一个测试函数,也是安装这个过程进行的;
$ TEST_ENV=aaa go test -v ./split.go ./split_test.go -test.run TestSplit
setup something
=== RUN TestSplit
--- PASS: TestSplit (0.00s)
PASS
tear down something
ok command-line-arguments 0.386s