在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
go test工具Go语言中的测试依赖 go test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以 在
测试函数测试函数的格式每个测试函数必须导入 func TestName(t *testing.T){ // ... } 测试函数的名字必须以 func TestAdd(t *testing.T){ ... } func TestSum(t *testing.T){ ... } func TestLog(t *testing.T){ ... } 其中参数 func (c *T) Error(args ...interface{}) func (c *T) Errorf(format string, args ...interface{}) func (c *T) Fail() func (c *T) FailNow() func (c *T) Failed() bool func (c *T) Fatal(args ...interface{}) func (c *T) Fatalf(format string, args ...interface{}) func (c *T) Log(args ...interface{}) func (c *T) Logf(format string, args ...interface{}) func (c *T) Name() string func (t *T) Parallel() func (t *T) Run(name string, f func(t *T)) bool func (c *T) Skip(args ...interface{}) func (c *T) SkipNow() func (c *T) Skipf(format string, args ...interface{}) func (c *T) Skipped() bool 测试函数示例就像细胞是构成我们身体的基本单位,一个软件程序也是由很多单元组件构成的。单元组件可以是函数、结构体、方法和最终用户可能依赖的任意东西。总之我们需要确保这些组件是能够正常运行的。单元测试是一些利用各种方法测试单元组件的程序,它会将结果与预期输出进行比较。 接下来,我们定义一个 // split/split.go package split import "strings" // split package with a single split function. // Split slices s into all substrings separated by sep and // returns a slice of the substrings between those separators. func Split(s, sep string) (result []string) { i := strings.Index(s, sep) for i > -1 { result = append(result, s[:i]) s = s[i+1:] i = strings.Index(s, sep) } result = append(result, s) return } 在当前目录下,我们创建一个 // split/split_test.go package split import ( "reflect" "testing" ) func TestSplit(t *testing.T) { // 测试函数名必须以Test开头,必须接收一个*testing.T类型参数 got := Split("a:b:c", ":") // 程序输出的结果 want := []string{"a", "b", "c"} // 期望的结果 if !reflect.DeepEqual(want, got) { // 因为slice不能比较直接,借助反射包中的方法比较 t.Errorf("excepted:%v, got:%v", want, got) // 测试失败输出错误提示 } } 此时 split $ ls -l total 16 -rw-r--r-- 1 liwenzhou staff 408 4 29 15:50 split.go -rw-r--r-- 1 liwenzhou staff 466 4 29 16:04 split_test.go 在 split $ go test PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.005s 一个测试用例有点单薄,我们再编写一个测试使用多个字符切割字符串的例子,在 func TestMoreSplit(t *testing.T) { got := Split("abcd", "bc") want := []string{"a", "d"} if !reflect.DeepEqual(want, got) { t.Errorf("excepted:%v, got:%v", want, got) } } 再次运行 split $ go test --- FAIL: TestMultiSplit (0.00s) split_test.go:20: excepted:[a d], got:[a cd] FAIL exit status 1 FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s 这一次,我们的测试失败了。我们可以为 split $ go test -v === RUN TestSplit --- PASS: TestSplit (0.00s) === RUN TestMoreSplit --- FAIL: TestMoreSplit (0.00s) split_test.go:21: excepted:[a d], got:[a cd] FAIL exit status 1 FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.005s 这一次我们能清楚的看到是 split $ go test -v -run="More" === RUN TestMoreSplit --- FAIL: TestMoreSplit (0.00s) split_test.go:21: excepted:[a d], got:[a cd] FAIL exit status 1 FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s 现在我们回过头来解决我们程序中的问题。很显然我们最初的 package split import "strings" // split package with a single split function. // Split slices s into all substrings separated by sep and // returns a slice of the substrings between those separators. func Split(s, sep string) (result []string) { i := strings.Index(s, sep) for i > -1 { result = append(result, s[:i]) s = s[i+len(sep):] // 这里使用len(sep)获取sep的长度 i = strings.Index(s, sep) } result = append(result, s) return } 这一次我们再来测试一下,我们的程序。注意,当我们修改了我们的代码之后不要仅仅执行那些失败的测试函数,我们应该完整的运行所有的测试,保证不会因为修改代码而引入了新的问题。 split $ go test -v === RUN TestSplit --- PASS: TestSplit (0.00s) === RUN TestMoreSplit --- PASS: TestMoreSplit (0.00s) PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s 这一次我们的测试都通过了。 测试组我们现在还想要测试一下 func TestSplit(t *testing.T) { // 定义一个测试用例类型 type test struct { input string sep string want []string } // 定义一个存储测试用例的切片 tests := []test{ {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}}, {input: "a:b:c", sep: ",", want: []string{"a:b:c"}}, {input: "abcd", sep: "bc", want: []string{"a", "d"}}, {input: "沙河有沙又有河", sep: "沙", want: []string{"河有", "又有河"}}, } // 遍历切片,逐一执行测试用例 for _, tc := range tests { got := Split(tc.input, tc.sep) if !reflect.DeepEqual(got, tc.want) { t.Errorf("excepted:%v, got:%v", tc.want, got) } } } 我们通过上面的代码把多个测试用例合到一起,再次执行 split $ go test -v === RUN TestSplit --- FAIL: TestSplit (0.00s) split_test.go:42: excepted:[河有 又有河], got:[ 河有 又有河] FAIL exit status 1 FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s 我们的测试出现了问题,仔细看打印的测试失败提示信息: 我们修改下测试用例的格式化输出错误提示部分: func TestSplit(t *testing.T) { ... for _, tc := range tests { got := Split(tc.input, tc.sep) if !reflect.DeepEqual(got, tc.want) { t.Errorf("excepted:%#v, got:%#v", tc.want, got) } } } 此时运行 split $ go test -v === RUN TestSplit --- FAIL: TestSplit (0.00s) split_test.go:42: excepted:[]string{"河有", "又有河"}, got:[]string{"", "河有", "又有河"} FAIL exit status 1 FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s 子测试看起来都挺不错的,但是如果测试用例比较多的时候,我们是没办法一眼看出来具体是哪个测试用例失败了。我们可能会想到下面的解决办法: func TestSplit(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 { 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格式化输出 } } } 上面的做法是能够解决问题的。同时Go1.7+中新增了子测试,我们可以按照如下方式使用 func TestSplit(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) { // 使用t.Run()执行子测试 got := Split(tc.input, tc.sep) if !reflect.DeepEqual(got, tc.want) { t.Errorf("excepted:%#v, got:%#v", tc.want, got) } }) } } 此时我们再执行 split $ go test -v === RUN TestSplit === RUN TestSplit/leading_sep === RUN TestSplit/simple === RUN TestSplit/wrong_sep === RUN TestSplit/more_sep --- FAIL: TestSplit (0.00s) --- FAIL: TestSplit/leading_sep (0.00s) split_test.go:83: excepted:[]string{"河有", "又有河"}, got:[]string{"", "河有", "又有河"} --- PASS: TestSplit/simple (0.00s) --- PASS: TestSplit/wrong_sep (0.00s) --- PASS: TestSplit/more_sep (0.00s) FAIL exit status 1 FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s 这个时候我们要把测试用例中的错误修改回来: func TestSplit(t *testing.T) { ... 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{"", "河有", "又有河"}}, } ... } 我们都知道可以通过 测试覆盖率测试覆盖率是你的代码被测试套件覆盖的百分比。通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。 Go提供内置功能来检查你的代码覆盖率。我们可以使用 split $ go test -cover PASS coverage: 100.0% of statements ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.005s 从上面的结果可以看到我们的测试用例覆盖了100%的代码。 Go还提供了一个额外的 split $ go test -cover -coverprofile=c.out PASS coverage: 100.0% of statements ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.005s 上面的命令会将覆盖率相关的信息输出到当前文件夹下面的
图中每个用绿色标记的语句块表示被覆盖了,而红色的表示没有被覆盖。 基准测试基准测试函数格式基准测试就是在一定的工作负载之下检测程序性能的一种方法。基准测试的基本格式如下: func BenchmarkName(b *testing.B){ // ... } 基准测试以 func (c *B) Error(args ...interface{}) func (c *B) Errorf(format string, args ...interface{}) func (c *B) Fail() func (c *B) FailNow() func (c *B) Failed() bool func (c *B) Fatal(args ...interface{}) func (c *B) Fatalf(format string, args ...interface{}) func (c *B) Log(args ...interface{}) func (c *B) Logf(format string, args ...interface{}) func (c *B) Name() string func (b *B) ReportAllocs() func (b *B) ResetTimer() func (b *B) Run(name string, f func(b *B)) bool func (b *B) RunParallel(body func(*PB)) func (b *B) SetBytes(n int64) func (b *B) SetParallelism(p int) func (c *B) Skip(args ...interface{}) func (c *B) SkipNow() func (c *B) Skipf(format string, args ...interface{}) func (c *B) Skipped() bool func (b *B) StartTimer() func (b *B) StopTimer()
基准测试示例我们为split包中的 func BenchmarkSplit(b *testing.B) { for i := 0; i < b.N; i++ { Split("沙河有沙又有河", "沙") } } 基准测试并不会默认执行,需要增加 split $ go test -bench=Split goos: darwin goarch: amd64 pkg: github.com/Q1mi/studygo/code_demo/test_demo/split BenchmarkSplit-8 10000000 203 ns/op PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 2.255s 其中 我们还可以为基准测试添加 split $ go test -bench=Split -benchmem goos: darwin goarch: amd64 pkg: github.com/Q1mi/studygo/code_demo/test_demo/split BenchmarkSplit-8 10000000 215 ns/op 112 B/op 3 allocs/op PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 2.394s 其中, func Split(s, sep string) (result []string) { result = make([]string, 0, strings.Count(s, sep)+1) i := strings.Index(s, sep) for i > -1 { result = append(result, s[:i]) s = s[i+len(sep):] // 这里使用len(sep)获取sep的长度 i = strings.Index(s, sep) } result = append(result, s) return } 这一次我们提前使用make函数将result初始化为一个容量足够大的切片,而不再像之前一样通过调用append函数来追加。我们来看一下这个改进会带来多大的性能提升: split $ go test -bench=Split -benchmem goos: darwin goarch: amd64 pkg: github.com/Q1mi/studygo/code_demo/test_demo/split BenchmarkSplit-8 10000000 127 ns/op 48 B/op 1 allocs/op PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 1.423s 这个使用make函数提前分配内存的改动,减少了2/3的内存分配次数,并且减少了一半的内存分配。 性能比较函数上面的基准测试只能得到给定操作的绝对耗时,但是在很多性能问题是发生在两个不同操作之间的相对耗时,比如同一个函数处理1000个元素的耗时与处理1万甚至100万个元素的耗时的差别是多少?再或者对于同一个任务究竟使用哪种算法性能最佳?我们通常需要对两个不同算法的实现使用相同的输入来进行基准比较测试。 性能比较函数通常是一个带有参数的函数,被多个不同的Benchmark函数传入不同的值来调用。举个例子如下: func benchmark(b *testing.B, size int){/* ... */} func Benchmark10(b *testing.B){ benchmark(b, 10) } func Benchmark100(b *testing.B){ benchmark(b, 100) } func Benchmark1000(b *testing.B){ benchmark(b, 1000) } 例如我们编写了一个计算斐波那契数列的函数如下: // fib.go // Fib 是一个计算第n个斐波那契数的函数 func Fib(n int) int { if n < 2 { return n } return Fib(n-1) + Fib(n-2) } 我们编写的性能比较函数如下: // fib_test.go func benchmarkFib(b *testing.B, n int) { for i := 0; i < b.N; i++ { Fib(n) } } func BenchmarkFib1(b *testing.B) { benchmarkFib(b, 1) } func BenchmarkFib2(b *testing.B) { benchmarkFib(b, 2) } func BenchmarkFib3(b *testing.B) { benchmarkFib(b, 3) } func BenchmarkFib10(b *testing.B) { benchmarkFib(b, 10) } func BenchmarkFib20(b *testing.B) { benchmarkFib(b, 20) } func BenchmarkFib40(b *testing.B) { benchmarkFib(b, 40) } 运行基准测试: split $ go test -bench=. goos: darwin goarch: amd64 pkg: github.com/Q1mi/studygo/code_demo/test_demo/fib BenchmarkFib1-8 1000000000 2.03 ns/op BenchmarkFib2-8 300000000 5.39 ns/op BenchmarkFib3-8 200000000 9.71 ns/op BenchmarkFib10-8 5000000 325 ns/op BenchmarkFib20-8 30000 42460 ns/op BenchmarkFib40-8 2 638524980 ns/op PASS ok github.com/Q1mi/studygo/code_demo/test_demo/fib 12.944s 这里需要注意的是,默认情况下,每个基准测试至少运行1秒。如果在Benchmark函数返回时没有到1秒,则b.N的值会按1,2,5,10,20,50,…增加,并且函数再次运行。 最终的BenchmarkFib40只运行了两次,每次运行的平均值只有不到一秒。像这种情况下我们应该可以使用 split $ go test -bench=Fib40 -benchtime=20s goos: darwin goarch: amd64 pkg: github.com/Q1mi/studygo/code_demo/test_demo/fib BenchmarkFib40-8 50 663205114 ns/op PASS ok github.com/Q1mi/studygo/code_demo/test_demo/fib 33.849s 这一次 使用性能比较函数做测试的时候一个容易犯的错误就是把 // 错误示范1 func BenchmarkFibWrong(b *testing.B) { for n := 0; n < b.N; n++ { Fib(n) } } // 错误示范2 func BenchmarkFibWrong2(b *testing.B) { Fib(b.N) } 重置时间
func BenchmarkSplit(b *testing.B) { time.Sleep(5 * time.Second) // 假设需要做一些耗时的无关操作 b.ResetTimer() // 重置计时器 for i := 0; i < b.N; i++ { Split("沙河有沙又有河", "沙") } } 并行测试
func BenchmarkSplitParallel(b *testing.B) { // b.SetParallelism(1) // 设置使用的CPU数 b.RunParallel(func(pb *testing.PB) { for pb.Next() { Split("沙河有沙又有河", "沙") } }) } 执行一下基准测试: split $ go test -bench=. goos: darwin goarch: amd64 pkg: github.com/Q1mi/studygo/code_demo/test_demo/split BenchmarkSplit-8 10000000 131 ns/op BenchmarkSplitParallel-8 50000000 36.1 ns/op PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 3.308s 还可以通过在测试命令后添加 Setup与TearDown测试程序有时需要在测试之前进行额外的设置(setup)或在测试之后进行拆卸(teardown)。 TestMain通过在 如果测试文件包含函数: 一个使用 func TestMain(m *testing.M) { fmt.Println("write setup code here...") // 测试之前的做一些设置 // 如果 TestMain 使用了 flags,这里应该加上flag.Parse() retCode := m.Run() // 执行测试 fmt.Println("write teardown code here...") // 测试之后做一些拆卸工作 os.Exit(retCode) // 退出测试 } 需要注意的是:在调用 子测试的Setup与Teardown有时候我们可能需要为每个测试集设置Setup与Teardown,也有可能需要为每个子测试设置Setup与Teardown。下面我们定义两个函数工具函数如下: // 测试集的Setup与Teardown func setupTestCase(t *testing.T) func(t *testing.T) { t.Log("如有需要在此执行:测试之前的setup") return func(t *testing.T) { t.Log("如有需要在此执行:测试之后的teardown") } } // 子测试的Setup与Teardown func setupSubTest(t *testing.T) func(t *testing.T) { t.Log("如有需要在此执行:子测试之前的setup") return func(t *testing.T) { t.Log("如有需要在此执行:子测试之后的teardown") } } 使用方式如下: func TestSplit(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{"", "河有", "又有河"}}, } teardownTestCase := setupTestCase(t) // 测试之前执行setup操作 defer teardownTestCase(t) // 测试之后执行testdoen操作 for name, tc := range tests { t.Run(name, func(t *testing.T) { // 使用t.Run()执行子测试 teardownSubTest := setupSubTest(t) // 子测试之前执行setup操作 defer teardownSubTest(t) // 测试之后执行testdoen操作 got := Split(tc.input, tc.sep) if !reflect.DeepEqual(got, tc.want) { t.Errorf("excepted:%#v, got:%#v", tc.want, got) } }) } } 测试结果如下: split $ go test -v === RUN TestSplit === RUN TestSplit/simple === RUN TestSplit/wrong_sep === RUN TestSplit/more_sep === RUN TestSplit/leading_sep --- PASS: TestSplit (0.00s) split_test.go:71: 如有需要在此执行:测试之前的setup --- PASS: TestSplit/simple (0.00s) split_test.go:79: 如有需要在此执行:子测试之前的setup split_test.go:81: 如有需要在此执行:子测试之后的teardown --- PASS: TestSplit/wrong_sep (0.00s) split_test.go:79: 如有需要在此执行:子测试之前的setup split_test.go:81: 如有需要在此执行:子测试之后的teardown --- PASS: TestSplit/more_sep (0.00s) split_test.go:79: 如有需要在此执行:子测试之前的setup split_test.go:81: 如有需要在此执行:子测试之后的teardown --- PASS: TestSplit/leading_sep (0.00s) split_test.go:79: 如有需要在此执行:子测试之前的setup split_test.go:81: 如有需要在此执行:子测试之后的teardown split_test.go:73: 如有需要在此执行:测试之后的teardown === RUN ExampleSplit --- PASS: ExampleSplit (0.00s) PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s 示例函数示例函数的格式被 func ExampleName() { // ... } 示例函数示例下面的代码是我们为 func ExampleSplit() { fmt.Println(split.Split("a:b:c", ":")) fmt.Println(split.Split("沙河有沙又有河", "沙")) // Output: // [a b c] // [ 河有 又有河] } 为你的代码编写示例代码有如下三个用处:
split $ go test -run Example PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s
|
请发表评论