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的值,这个对于并发基准测试很重要。1489494794ns/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
posted @ 2020-11-25 17:42  KpHang  阅读(105)  评论(0编辑  收藏  举报