golang 1.7之後進階測試方法之子測試,子基準測試(subtest sub-benchmarks)

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

介紹

在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後跟以'/'分割的Regex,用來制定特定的測試案例。

  • 執行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 mainimport (    "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/opBenchmarkProductInfo/p7-4                  10000            108921 ns/opBenchmarkProductInfo/p666-4                10000            107163 ns/opBenchmarkProductInfoParalle-4              10000            113386 ns/opBenchmarkProductLock/pn3-4                 10000            100418 ns/opBenchmarkProductLock/p7-4                  20000             97373 ns/opBenchmarkProductLock/p666-4                20000             96905 ns/opBenchmarkProductLockParallel-4             10000            108399 ns/op
  • 執行如下測試命令

    go test -bench=ProductInfo

    過濾測試函數名中包含ProductInfo的測試案例,結果:

    BenchmarkProductInfo/pn3-4                 10000            111065 ns/opBenchmarkProductInfo/p7-4                  10000            118515 ns/opBenchmarkProductInfo/p666-4                10000            111723 ns/opBenchmarkProductInfoParalle-4              10000            118641 ns/op
  • 執行如下測試命令

    go test -bench=oductInfo

    過濾測試函數名中包含oductInfo的測試案例,結果:

    BenchmarkProductInfo/pn3-4                 10000            107338 ns/opBenchmarkProductInfo/p7-4                  10000            109848 ns/opBenchmarkProductInfo/p666-4                10000            109344 ns/opBenchmarkProductInfoParalle-4              10000            114351 ns/op
  • 執行如下測試命令

    go test -bench=ProductInfo/p7

    過濾測試函數名中包含ProductInfo且子測試名稱包含p7的測試案例,同時我們可以注意到並行的測試也執行了。結果:

    BenchmarkProductInfo/p7-4                  10000            109045 ns/opBenchmarkProductInfoParalle-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篇:極速提升,逐步超越
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.