加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 大数据 > 正文

golang 1.7之后高级测试方法之子测试,子基准测试(subtest sub-b

发布时间:2020-12-16 19:08:34 所属栏目:大数据 来源:网络整理
导读:介绍 在go1.7之后,testing包T和B的引入了一个Run方法,用于创建subtests 和 sub-benchmarks. subtests 和 sub-benchmarks可以让开发者更好的处理测试中的失败,更好的控制运行哪个测试用例,控制并行测试操作,测试代码更加简洁和可维护性更强。 Table-driv

介绍

在go1.7之后,testing包T和B的引入了一个Run方法,用于创建subtests 和 sub-benchmarks. subtests 和 sub-benchmarks可以让开发者更好的处理测试中的失败,更好的控制运行哪个测试用例,控制并行测试操作,测试代码更加简洁和可维护性更强。

Table-driven tests 基础

首先我们先讨论下Go中常见的测试代码编写方式。

一系列相关的测试校验可以通过遍历测试用例的切片来实现,代码如下:

func TestTime(t *testing.T) {
    testCases := []struct {
        gmt  string
        loc  string
        want string
    }{
        {"12:31","Europe/Zuri","13:31"},// incorrect location name
        {"12:31","America/New_York","7:31"},// should be 07:31
        {"08:08","Australia/Sydney","18:08"},}
    for _,tc := range testCases {
        loc,err := time.LoadLocation(tc.loc)
        if err != nil {
            t.Fatalf("could not load location %q",tc.loc)
        }
        gmt,_ := time.Parse("15:04",tc.gmt)
        if got := gmt.In(loc).Format("15:04"); got != tc.want {
            t.Errorf("In(%s,%s) = %s; want %s",tc.gmt,tc.loc,got,tc.want)
        }
    }
}

测试函数必须以Test开头,Test后跟的名字也必须首字母大写。
上面的测试方式称为table-driven 测试法,可以降低重复代码。

Table-driven benchmarks

在go1.7之前是不能够对benchmarks采用table-driven的方法的,如果要测试不同的参数就需要编写不同的benchmark函数,在go1.7之前常见的benchmarks测试代码如下:

func benchmarkAppendFloat(b *testing.B,f float64,fmt byte,prec,bitSize int) {
    dst := make([]byte, 30)
    b.ResetTimer() // Overkill here,but for illustrative purposes.
    for i := 0; i < b.N; i++ {
        AppendFloat(dst[:0],f,fmt,bitSize)
    }
}

func BenchmarkAppendFloatDecimal(b *testing.B) { benchmarkAppendFloat(b, 33909,'g', -1, 64) }
func BenchmarkAppendFloat(b *testing.B)        { benchmarkAppendFloat(b, 339.7784, 64) }
func BenchmarkAppendFloatExp(b *testing.B)     { benchmarkAppendFloat(b, -5.09e75, 64) }
func BenchmarkAppendFloatNegExp(b *testing.B)  { benchmarkAppendFloat(b, -5.11e-95, 64) }
func BenchmarkAppendFloatBig(b *testing.B)     { benchmarkAppendFloat(b, 123456789123456789123456789, 64) }

go1.7之后,采用table-drive方法代码如下:

func BenchmarkAppendFloat(b *testing.B) {
    benchmarks := []struct{
        name    string
        float   float64
        fmt     byte
        prec    int
        bitSize int
    }{
        {"Decimal", 64},{"Float",{"Exp",{"NegExp",{"Big",...
    }
    dst := make([]byte, 30)
    for _,bm := range benchmarks {
        b.Run(bm.name,func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                AppendFloat(dst[:0],bm.float,bm.fmt,bm.prec,bm.bitSize)
            }
        })
    }
}

每个b.Run单独创建一个benchmark。
可以看到新的编码方式可读性和可维护行上更强。

如果想要子测试并发执行,则使用 b.RunParallel

Table-driven tests using subtests

Go1.7之后引用Run方法用于创建subtests,对之前 Table-driven tests 基础 中的代码重新写为:

func TestTime(t *testing.T) {
    testCases := []struct {
        gmt  string
        loc  string
        want string
    }{
        {"12:31",{"12:31",{"08:08",tc := range testCases {
        t.Run(fmt.Sprintf("%s in %s",tc.loc),func(t *testing.T) {
            loc,err := time.LoadLocation(tc.loc)
            if err != nil {
                t.Fatal("could not load location")
            }
            gmt,tc.gmt)
            if got := gmt.In(loc).Format("15:04"); got != tc.want {
                t.Errorf("got %s; want %s",tc.want)
            }
        })
    }
}

go1.7之前的 Table-driven tests 基础 的测试代码运行结果为:

--- FAIL: TestTime (0.00s)
    time_test.go:62: could not load location "Europe/Zuri"

虽然两个用例都是错误的,但是 第一个用例Fatalf 后,后面的用例也就没能进行运行。

使用Run的测试代码运行结果为:

--- FAIL: TestTime (0.00s)     --- FAIL: TestTime/12:31_in_Europe/Zuri (0.00s)         time_test.go:84: could not load location
    --- FAIL: TestTime/12:31_in_America/New_York (0.00s)         time_test.go:88: got 07:31; want 7:31

Fatal 导致subtest被跳过,不过不影响其他subtest以及父test的测试。

针对每一个子测试,go test命令都会打印出一行测试摘要。它们是分离的、独立统计的。这可以让我们进行更加精细的测试,细到每次输入输出。

过滤执行测试用例

subtests和sub-benchmarks可以使用 -run or -bench flag
来对测试用例进行过滤运行。 -run or -bench flag后跟以’/’分割的正则表达式,用来制定特定的测试用例。

  • 执行TestTime下匹配”in Europe” 的子测试
$ go test -run=TestTime/"in Europe"
--- FAIL: TestTime (0.00s)
    --- FAIL: TestTime/12:31_in_Europe/Zuri (0.00s)
        time_test.go:85: could not load location
  • 执行TestTime下匹配”12:[0-9] ” 的子测试
$ go test -run=Time/12:[0-9] -v
=== RUN TestTime
=== RUN TestTime/12:31_in_Europe/Zuri
=== RUN TestTime/12:31_in_America/New_York
--- FAIL: TestTime (0.00s)
 --- FAIL: TestTime/12:31_in_Europe/Zuri (0.00s)
 time_test.go:85: could not load location
 --- FAIL: TestTime/12:31_in_America/New_York (0.00s)
 time_test.go:89: got 07:31; want 7:31
$ go test -run=Time//New_York
--- FAIL: TestTime (0.00s)     --- FAIL: TestTime/12:31_in_America/New_York (0.00s)         time_test.go:88: got 07:31; want 7:31

func (*T) Parallel

func (t *T) Parallel()

使用t.Parallel(),使测试和其它子测试并发执行。

tc := tc这个地方很关键,不然多个子测试可能使用的tc是同一个。
func TestGroupedParallel(t *testing.T) {
    for _,tc := range testCases {
        tc := tc // capture range variable
        t.Run(tc.Name,func(t *testing.T) {
            t.Parallel()
            if got := foo(tc.in); got != tc.out {
                t.Errorf("got %v; want %v",tc.out)
            }
            ...
        })
    }
}

func (*B) RunParallel

func (b *B) RunParallel(body func(*PB))

RunParallel runs a benchmark in parallel. It creates multiple goroutines and distributes b.N iterations among them. The number of goroutines defaults to GOMAXPROCS. To increase parallelism for non-CPU-bound benchmarks,call SetParallelism before RunParallel. RunParallel is usually used with the go test -cpu flag.

The body function will be run in each goroutine. It should set up any goroutine-local state and then iterate until pb.Next returns false. It should not use the StartTimer,StopTimer,or ResetTimer functions,because they have global effect. It should also not call Run.

RunParallel并发的执行benchmark。RunParallel创建多个goroutine然后把b.N个迭代测试分布到这些goroutine上。goroutine的数目默认是GOMAXPROCS。如果要增加non-CPU-bound的benchmark的并个数,在执行RunParallel之前调用SetParallelism。

不要使用 StartTimer,or ResetTimer functions这些函数,因为这些函数都是 global effect的。

package main

import (
    "bytes"
    "testing"
    "text/template"
)

func main() {
    // Parallel benchmark for text/template.Template.Execute on a single object.
    testing.Benchmark(func(b *testing.B) {
        templ := template.Must(template.New("test").Parse("Hello,{{.}}!"))
        // RunParallel will create GOMAXPROCS goroutines
        // and distribute work among them.
        b.RunParallel(func(pb *testing.PB) {
            // Each goroutine has its own bytes.Buffer.
            var buf bytes.Buffer
            for pb.Next() {
                // The loop body is executed b.N times total across all goroutines.
                buf.Reset()
                templ.Execute(&buf,"World")
            }
        })
    })
}

本人测试实例

Benchmark测试代码

func BenchmarkProductInfo(b *testing.B) {
    // b.ResetTimer()

    testCases := []string{"pn3","p7","p666"}
    for _,productId := range testCases {
        // b.SetParallelism
        b.Run(productId,func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                mgoDB.ecnGetProductInfoOfProductId(productId)
            }
        })
    }
}

func BenchmarkProductInfoParalle(b *testing.B) {
    // b.ResetTimer()

    testCases := []string{"pn3",tproductId := range testCases {
        // b.SetParallelism
        productId := tproductId
        b.RunParallel(func(b *testing.PB) {
            for b.Next() {
                mgoDB.ecnGetProductInfoOfProductId(productId)
            }

        })
    }
}

func BenchmarkProductLock(b *testing.B) {
    // b.ResetTimer()
    testCases := []string{"pn3",func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                mgoDB.CheckProductLockStatus(productId)
            }
        })
    }

}
func BenchmarkProductLockParallel(b *testing.B) {
    // b.ResetTimer()
    testCases := []string{"pn3",tproductId := range testCases {
        // b.SetParallelism
        productId := tproductId
        b.RunParallel(func(b *testing.PB) {
            for b.Next() {
                mgoDB.CheckProductLockStatus(productId)
            }
        })
    }

}
  • 执行如下测试命令
go test -bench="."

结果

BenchmarkProductInfo/pn3-4                 10000            107704 ns/op
BenchmarkProductInfo/p7-4                  10000            108921 ns/op
BenchmarkProductInfo/p666-4                10000            107163 ns/op
BenchmarkProductInfoParalle-4              10000            113386 ns/op
BenchmarkProductLock/pn3-4                 10000            100418 ns/op
BenchmarkProductLock/p7-4                  20000             97373 ns/op
BenchmarkProductLock/p666-4                20000             96905 ns/op
BenchmarkProductLockParallel-4             10000            108399 ns/op
  • 执行如下测试命令
 go test -bench=ProductInfo

过滤测试函数名中包含ProductInfo的测试用例,结果:

BenchmarkProductInfo/pn3-4                 10000            111065 ns/op
BenchmarkProductInfo/p7-4                  10000            118515 ns/op
BenchmarkProductInfo/p666-4                10000            111723 ns/op
BenchmarkProductInfoParalle-4              10000            118641 ns/op
  • 执行如下测试命令
go test -bench=oductInfo

过滤测试函数名中包含oductInfo的测试用例,结果:

BenchmarkProductInfo/pn3-4                 10000            107338 ns/op
BenchmarkProductInfo/p7-4                  10000            109848 ns/op
BenchmarkProductInfo/p666-4                10000            109344 ns/op
BenchmarkProductInfoParalle-4              10000            114351 ns/op
  • 执行如下测试命令
 go test -bench=ProductInfo/p7

过滤测试函数名中包含ProductInfo且子测试名称包含p7的测试用例,同时我们可以注意到并行的测试也执行了。结果:

BenchmarkProductInfo/p7-4                  10000            109045 ns/op
BenchmarkProductInfoParalle-4              10000            117569 ns/op

Test测试代码

func TestCheckProductLockt(t *testing.T) {
    testCases := []string{"a1","a2","a3"}
    for _,productID := range testCases {

        t.Log(productID)
        t.Run(productID,func(t *testing.T) {
            _,ret := mgoDB.ecnGetProductInfoOfProductId(productID)
            if ret != Success {
                t.Fatalf("faield")
            }

        })

    }
}

func TestCheckProductLocktParalle(t *testing.T) {
    testCases := []string{"a1",tproductID := range testCases {
        productID := tproductID
        t.Log(productID)
        t.Run(productID,func(t *testing.T) {
            t.Parallel()
            _,ret := mgoDB.ecnGetProductInfoOfProductId(productID)
            if ret != Success {
                t.Fatalf("faield")
            }

        })

    }
}

func TestUserIDMatchRole(t *testing.T) {
    reqData := []struct {
        ProductID string
        UserID    string
        RoleType  string
    }{
        {"pn2","48176d26e860975e96518b80a3520407","HR"},{"pn2","CEO"},"CTO"},}

    for _,data := range reqData {
        //
        t.Log(data)
        t.Run(fmt.Sprint("%s %s",data.ProductID,data.RoleType),func(t *testing.T) {
            if ret := checkUserMatchProductRole(data.ProductID,data.UserID,data.RoleType); ret != Success {
                t.Error("not match")
            }
        })

    }
}

func TestUserIDMatchRoleParall(t *testing.T) {
    reqData := []struct {
        ProductID string
        UserID    string
        RoleType  string
    }{
        {"pn2",tdata := range reqData {
        //
        data := tdata //重要
        t.Log(data)
        t.Run(fmt.Sprint("%s %s",func(t *testing.T) {
            t.Parallel()
            if ret := checkUserMatchProductRole(data.ProductID,data.RoleType); ret != Success {
                t.Error("not match")
            }
        })

    }
}
  • 执行如下测试命令
go test -bench="."

结果

--- FAIL: TestCheckProductLockt (0.00s)         ecn_test.go:626: a1
    --- FAIL: TestCheckProductLockt/a1 (0.00s)         ecn_test.go:630: faield
        ecn_test.go:626: a2
    --- FAIL: TestCheckProductLockt/a2 (0.00s)         ecn_test.go:630: faield
        ecn_test.go:626: a3
    --- FAIL: TestCheckProductLockt/a3 (0.00s)         ecn_test.go:630: faield
--- FAIL: TestCheckProductLocktParalle (0.00s)         ecn_test.go:642: a1
        ecn_test.go:642: a2
        ecn_test.go:642: a3
    --- FAIL: TestCheckProductLocktParalle/a1 (0.00s)         ecn_test.go:647: faield
    --- FAIL: TestCheckProductLocktParalle/a2 (0.00s)         ecn_test.go:647: faield
    --- FAIL: TestCheckProductLocktParalle/a3 (0.00s)         ecn_test.go:647: faield
--- FAIL: TestUserIDMatchRole (0.00s)         ecn_test.go:668: {pn2 48176d26e860975e96518b80a3520407 HR}
    --- FAIL: TestUserIDMatchRole/%s_%spn2HR (0.00s)         ecn_test.go:671: not match
        ecn_test.go:668: {pn2 48176d26e860975e96518b80a3520407 CEO}
    --- FAIL: TestUserIDMatchRole/%s_%spn2CEO (0.00s)         ecn_test.go:671: not match
        ecn_test.go:668: {pn2 48176d26e860975e96518b80a3520407 CTO}
    --- FAIL: TestUserIDMatchRole/%s_%spn2CTO (0.00s)         ecn_test.go:671: not match
--- FAIL: TestUserIDMatchRoleParall (0.00s)         ecn_test.go:692: {pn2 48176d26e860975e96518b80a3520407 HR}
        ecn_test.go:692: {pn2 48176d26e860975e96518b80a3520407 CEO}
        ecn_test.go:692: {pn2 48176d26e860975e96518b80a3520407 CTO}
    --- FAIL: TestUserIDMatchRoleParall/%s_%spn2HR (0.00s)         ecn_test.go:696: not match
    --- FAIL: TestUserIDMatchRoleParall/%s_%spn2CTO (0.00s)         ecn_test.go:696: not match
    --- FAIL: TestUserIDMatchRoleParall/%s_%spn2CEO (0.00s)         ecn_test.go:696: not match

在测试代码中我们添加了t.log的打印,通过打印对比并发版本和非并发版本的输出,可以看到非并发版本的测试的确时顺序执行的,而并发版本的测试是并发执行的。

参考网址

Using Subtests and Sub-benchmarks
解读2016之Golang篇:极速提升,逐步超越

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读