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

kingeasternsun · · 1185 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

介绍

在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, prec, bitSize)
    }
}

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

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

func BenchmarkAppendFloat(b *testing.B) {
    benchmarks := []struct{
        name    string
        float   float64
        fmt     byte
        prec    int
        bitSize int
    }{
        {"Decimal", 33909, 'g', -1, 64},
        {"Float", 339.7784, 'g', -1, 64},
        {"Exp", -5.09e75, 'g', -1, 64},
        {"NegExp", -5.11e-95, 'g', -1, 64},
        {"Big", 123456789123456789123456789, 'g', -1, 64},
        ...
    }
    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", "Europe/Zuri", "13:31"},
        {"12:31", "America/New_York", "7:31"},
        {"08:08", "Australia/Sydney", "18:08"},
    }
    for _, tc := range testCases {
        t.Run(fmt.Sprintf("%s in %s", tc.gmt, tc.loc), func(t *testing.T) {
            loc, err := time.LoadLocation(tc.loc)
            if err != nil {
                t.Fatal("could not load location")
            }
            gmt, _ := time.Parse("15:04", tc.gmt)
            if got := gmt.In(loc).Format("15:04"); got != tc.want {
                t.Errorf("got %s; want %s", got, 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", got, 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, StopTimer, 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", "p7", "p666"}
    for _, 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", "p7", "p666"}
    for _, productId := range testCases {
        // b.SetParallelism
        b.Run(productId, func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                mgoDB.CheckProductLockStatus(productId)
            }
        })
    }

}
func BenchmarkProductLockParallel(b *testing.B) {
    // b.ResetTimer()
    testCases := []string{"pn3", "p7", "p666"}
    for _, 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", "a2", "a3"}
    for _, 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", "48176d26e860975e96518b80a3520407", "CEO"},
        {"pn2", "48176d26e860975e96518b80a3520407", "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", "48176d26e860975e96518b80a3520407", "HR"},
        {"pn2", "48176d26e860975e96518b80a3520407", "CEO"},
        {"pn2", "48176d26e860975e96518b80a3520407", "CTO"},
    }

    for _, tdata := range reqData {
        //
        data := tdata //重要
        t.Log(data)
        t.Run(fmt.Sprint("%s %s", data.ProductID, data.RoleType), func(t *testing.T) {
            t.Parallel()
            if ret := checkUserMatchProductRole(data.ProductID, data.UserID, 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篇:极速提升,逐步超越

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:kingeasternsun

查看原文:golang 1.7之后高级测试方法之子测试,子基准测试(subtest sub-benchmarks)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1185 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传