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

介绍

在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篇:极速提升,逐步超越

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容