Go Test測試簡介

Go Test測試簡介

Go語言擁有一套單元測試和性能測試系統(tǒng),僅需要添加很少的代碼就可以快速測試一段需求代碼。go test 命令,會自動讀取源碼目錄下面名為 *_test.go 的文件,生成并運行測試用的可執(zhí)行文件并輸出結(jié)果信息。

本文中測試用例來源于golang官方文檔并且測試結(jié)果為golang 1.12.5下完成

測試類型

go test的測試類型分為以下三種

  • 單元測試,測試函數(shù)需要以Test為前綴,例如:

    func TestXxx(*testing.T)
    

    注意:Xxx 可以是任何字母數(shù)字字符串,但是第一個字母不能是小些字母。

    在這些函數(shù)中,使用 Error, Fail 或相關(guān)方法來發(fā)出失敗信號。

    要編寫一個新的測試套件,需要創(chuàng)建一個名稱以 _test.go 結(jié)尾的文件,該文件包含 TestXxx 函數(shù),如上所述。 將該文件放在與被測試的包相同的包中。該文件將被排除在正常的程序包之外,但在運行 “ go test ” 命令時將被包含。 有關(guān)詳細(xì)信息,請運行 “ go help test ” 和 “ go help testflag ” 了解。

    如果有需要,可以調(diào)用 *T*B 的 Skip 方法,跳過該測試或基準(zhǔn)測試:

    func TestTimeConsuming(t *testing.T) {
        if testing.Short() {
            t.Skip("skipping test in short mode.")
        }
        // ...
    }
    

    注意:testing.Short()判斷是否-test.short flag是否被設(shè)置(后續(xù)示例會進行演示)

  • 基準(zhǔn)測試,測試函數(shù)需要以Benchmark為前綴,例如:

      func BenchmarkXxx(*testing.B)
    

    通過 "go test" 命令,加上 -bench flag 來執(zhí)行,多個基準(zhǔn)測試按照順序運行。

    基準(zhǔn)測試函數(shù)如下所示:

    func BenchmarkHelloWorld(b *testing.B) {
        for i := 0; i < b.N; i++ {
            fmt.Sprintf("Hello World")
        }
    }
    

    基準(zhǔn)函數(shù)會運行目標(biāo)代碼 b.N 次。在基準(zhǔn)執(zhí)行期間,會調(diào)整 b.N 直到基準(zhǔn)測試函數(shù)持續(xù)足夠長的時間

    BenchmarkHelloWorld    1000000    216 ns/op
    

    意味著次基準(zhǔn)測試函數(shù)在測試時間內(nèi)循環(huán)執(zhí)行了 1000000 次,每次循環(huán)花費 2216 納秒 (ns)。

    注意:為了縮短基準(zhǔn)測試,請使用-benchtime命令行標(biāo)志設(shè)置基準(zhǔn)測試運行時間,而不要使用testing.short()函數(shù)跳過部分基準(zhǔn)測試。

  • 子測試

    單元測試和基準(zhǔn)測試的Run方法允許定義子測試和子基準(zhǔn)測試,而不必為每個子函數(shù)定義單獨的功能。 這使得諸如table-driven測試以及creating hierarchical成為了可能。 它還提供了一種共享通用設(shè)置和拆卸代碼的方法:

    func TestFoo(t *testing.T) {
      // <setup code>
      t.Run("A=1", func(t *testing.T) { fmt.Println("A=1") })
      t.Run("A=2", func(t *testing.T) { fmt.Println("A=2") })
      t.Run("B=1", func(t *testing.T) { fmt.Println("B=1") })
      // <tear-down code>
    }
    

    每個子測試和子基準(zhǔn)測試都有一個唯一的名稱:頂級測試的名稱和傳遞給 Run 的名稱的組合,以斜杠分隔,并具有用于消歧的可選尾隨序列號。

code示例&執(zhí)行解析

待測試的函數(shù)代碼

func Fib(n int) int {
        if n < 2 {
                return n
        }
        return Fib(n-1) + Fib(n-2)
}
  • 單元測試

    package codetest
    
    import "testing"
    
    func TestFib(t *testing.T) {
        var (
            in       = 7
            expected = 13
        )
        actual := Fib(in)
        if actual != expected {
            t.Errorf("Fib(%d) = %d; expected %d", in, actual, expected)
        }
    }
    

    執(zhí)行go test .輸出結(jié)果

    $ go test .
    ok      _/Users/xxxxx/Workspace/test/src/codetest   0.005s`
    

    此結(jié)果表示測試通過。

    我們可以在文件fib_test.go新增一個測試函數(shù)TestFib1

    func TestFib1(t *testing.T) {
        var (
            in       = 7
            expected = 14
        )
        actual := Fib(in)
        if actual != expected {
            t.Errorf("Fib(%d) = %d; expected %d", in, actual, expected)
        }
    }
    

    執(zhí)行go test .輸出結(jié)果

    $ go test .
    --- FAIL: TestFib1 (0.00s)
        fib_test.go:23: Fib(7) = 13; expected 14
    FAIL
    FAIL    _/Users/xxxxx/Workspace/test/src/codetest   0.005s
    

    可以看出由于TestFib1測試函數(shù)失敗進而導(dǎo)致整體的單元測試失敗。

    運行指定單元測試用例

    go test指定文件時默認(rèn)執(zhí)行文件內(nèi)的所有測試用例??梢允褂?code>-run參數(shù)選擇需要的測試用例單獨執(zhí)行,參考一下的代碼。

    package codetest
    
    import "testing"
    
    func TestCodeA(t *testing.T) {
      t.Log("A")
    }
    func TestCodeAC(t *testing.T) {
      t.Log("AC")
    }
    func TestCodeB(t *testing.T) {
      t.Log("B")
    }
    func TestCodeC(t *testing.T) {
      t.Log("C")
    }
    

    當(dāng)指定只執(zhí)行TestCodeA開頭的函數(shù)時,可以使用如下命令:

    go test -v -run TestCodeAgo test -v -run ^TestCodeA[a-zA-Z0-9]*$

    輸出結(jié)果如下:

    === RUN   TestCodeA
    --- PASS: TestCodeA (0.00s)
        code_test.go:6: A
    === RUN   TestCodeAC
    --- PASS: TestCodeAC (0.00s)
        code_test.go:9: AC
    PASS
    ok    _/Users/xxxxx/Workspace/test/src/codetest   0.005s
    

    Table-driven覆蓋測試

    當(dāng)在測試中需要進行多case的覆蓋測試,如果每次都以修改單元測試函數(shù)中的方式實現(xiàn),這樣無疑顯得很笨拙。在此情況下,我們可以采用 Table-driven 的方式寫測試。我們在lib_test.go中新增一個測試函數(shù)TestFibMore

    func TestFibMore(t *testing.T) {
        var fibTests = []struct {
            in       int // input
            expected int // expected result
        }{
            {1, 1},
            {2, 1},
            {3, 2},
            {4, 3},
            {5, 5},
            {6, 8},
            {7, 13},
        }
    
        for _, test := range fibTests {
            actual := Fib(test.in)
            if actual != test.expected {
                t.Errorf("Fib(%d) = %d; expected %d", tt.in, actual, tt.expected)
            }
        }
    }
    
    $ go test -run TestFibMore
    PASS
    ok      _/Users/xxxxx/Workspace/test/src/codetest   0.005s
    

    因為我們使用的是 t.Errorf,其中某個 case 失敗,并不會終止測試執(zhí)行。如下所示:

    TestFibMore函數(shù)case中的{7,13}改為{7,14},并再次運行測試

    $ go test -run TestFibMore
    --- FAIL: TestFibMore (0.00s)
        fib_test.go:44: Fib(7) = 13; expected 14
    FAIL
    exit status 1
    FAIL    _/Users/xxxxx/Workspace/test/src/codetest   0.004s
    
  • 基準(zhǔn)測試

    func BenchmarkFib(b *testing.B) {
            for n := 0; n < b.N; n++ {
                    Fib(10)
            }
    }
    

    運行指定基準(zhǔn)測試用例

    若單元測試函數(shù)與基準(zhǔn)測試函數(shù)在同一包下時或同一個文件時,可以通過在命令中添加 -run=none或者-run=^$來規(guī)避掉單元測試的運行

    運行測試命令go test -run=none -bench .后輸出:

    go test -run=none -bench .
    goos: darwin
    goarch: amd64
    BenchmarkFib-12          5000000               295 ns/op
    PASS
    ok      _/Users/xxxxx/Workspace/test/src/codetest   1.791s
    

    指定運基準(zhǔn)測試運行時間

    我們還可以通過在命令中加入 -benchtime=60m來設(shè)置基準(zhǔn)測試的運行時間。

    $ go test -run=none -benchtime=10s  -bench .
    goos: darwin
    goarch: amd64
    BenchmarkFib-12         50000000               306 ns/op
    PASS
    ok      _/Users/xxxxx/Workspace/test/src/codetest   15.618s
    

    嵌套基準(zhǔn)測試函數(shù)

    同時我們還可以通過創(chuàng)建一個基礎(chǔ)函數(shù)并在此基礎(chǔ)上衍生出更多基準(zhǔn)測試的方法來進行多case情況的測試

    func BenchmarkFib1(b *testing.B)  { benchmarkFib(1, b) }
    func BenchmarkFib2(b *testing.B)  { benchmarkFib(2, b) }
    func BenchmarkFib3(b *testing.B)  { benchmarkFib(3, b) }
    func BenchmarkFib10(b *testing.B) { benchmarkFib(10, b) }
    func BenchmarkFib20(b *testing.B) { benchmarkFib(20, b) }
    func BenchmarkFib40(b *testing.B) { benchmarkFib(40, b) }
    
    func benchmarkFib(i int, b *testing.B) {
      for n := 0; n < b.N; n++ {
          Fib(i)
      }
    }
    

    運行測試命令go test -run=none -bench .后輸出:

    $ go test -run=none -bench .
    goos: darwin
    goarch: amd64
    BenchmarkFib1-12        2000000000               1.54 ns/op
    BenchmarkFib2-12        300000000                4.97 ns/op
    BenchmarkFib3-12        200000000                8.07 ns/op
    BenchmarkFib10-12        5000000               298 ns/op
    BenchmarkFib20-12          50000             38077 ns/op
    BenchmarkFib40-12              2         589032380 ns/op
    PASS
    ok      _/Users/xxxxx/Workspace/test/src/codetest   13.554s
    

    注意:輸出中的-12應(yīng)為系統(tǒng)的最大核心數(shù)

    內(nèi)存統(tǒng)計

    在運行相關(guān)基準(zhǔn)測試時,可以使用-benchmem參數(shù)查看基準(zhǔn)測試的內(nèi)存使用情況。

    $ go test -run=none -benchmem -bench .
    goos: darwin
    goarch: amd64
    BenchmarkFib1-12        2000000000               1.55 ns/op            0 B/op          0 allocs/op
    BenchmarkFib2-12        300000000                4.86 ns/op            0 B/op          0 allocs/op
    BenchmarkFib3-12        200000000                7.96 ns/op            0 B/op          0 allocs/op
    BenchmarkFib10-12        5000000               295 ns/op               0 B/op          0 allocs/op
    BenchmarkFib20-12          50000             36733 ns/op               0 B/op          0 allocs/op
    BenchmarkFib40-12              2         555686536 ns/op               0 B/op          0 allocs/op
    PASS
    ok      _/Users/xxxxx/Workspace/test/src/codetest   13.327s
    

    從運行結(jié)果中看出在測試結(jié)果中增加了兩列數(shù)據(jù)分別為xx B/opxx allocs/op,分別代表在-一次循環(huán)中分配的內(nèi)存大小和一次循環(huán)中進行了多少次內(nèi)存的分配。

    計時器的使用

    1. StartTimer()

      開始計時測試。在基準(zhǔn)測試開始之前會自動調(diào)用此函數(shù),但也可以用于在調(diào)用StopTimer之后恢復(fù)計時。

    2. StopTimer()

      停止計時測試。 這可用于在執(zhí)行您不想測量的復(fù)雜初始化時暫停計時器。

    3. ResetTimer()

      將經(jīng)過的基準(zhǔn)時間和內(nèi)存分配計數(shù)器歸零,并刪除用戶報告的指標(biāo)。 它不影響計時器是否正在運行。

    func BenchmarkBigLen(b *testing.B) {
      // If a benchmark needs some expensive setup 
      // before running, the timer may be reset
      big := NewBig() 
      b.ResetTimer()
      for i := 0; i < b.N; i++ {
          big.Len()
      }
    }
    

    上面的示例來源于go官方testing package中,我也沒有在golang本身的代碼庫中找到NewBig函數(shù)的定義,推測只是代表耗時操作吧。

    并行執(zhí)行基準(zhǔn)測試

    如果基準(zhǔn)測試需要在并行設(shè)置中測試性能,則可以使用 RunParallel 輔助函數(shù) ; 這樣的基準(zhǔn)測試一般與 go test -cpu 標(biāo)志一起使用。RunParallel 會創(chuàng)建出多個 goroutine,并將 b.N 分配給這些 goroutine 執(zhí)行,其中 goroutine 數(shù)量的默認(rèn)值為 GOMAXPROCS。如果想要增加goroutine的數(shù)量,可以調(diào)用函數(shù)SetParallelism將RunParallel使用的goroutine的數(shù)量設(shè)置為p * GOMAXPROCS。RunParallel 函數(shù)將在每個 goroutine 中執(zhí)行,這個函數(shù)需要設(shè)置所有 goroutine 本地的狀態(tài),并迭代直到 pb.Next 返回 false 值為止。

    func BenchmarkTemplateParallel(b *testing.B) {
          // b.SetParallelism(2)
        templ := template.Must(template.New("test").Parse("Hello, {{.}}!"))
        b.RunParallel(func(pb *testing.PB) {
             // 每個 goroutine 有屬于自己的 bytes.Buffer.
            var buf bytes.Buffer
            for pb.Next() {
                  // 所有 goroutine 一起,循環(huán)一共執(zhí)行 b.N 次
                buf.Reset()
                templ.Execute(&buf, "World")
            }
        })
    }
    

    運行結(jié)果

    $ go test -run=none -cpu=6  -bench .
    goos: darwin
    goarch: amd64
    BenchmarkTemplateParallel-6     30000000                48.9 ns/op
    PASS
    ok      _/Users/xxxxx/Workspace/test/src/codetest   1.531s
    

    基準(zhǔn)測試結(jié)果

    BenchmarkResult結(jié)構(gòu)包含了基準(zhǔn)測試運行后的結(jié)果。

    type BenchmarkResult struct {
        N         int           // The number of iterations.
        T         time.duration // The total time taken.
        Bytes     int64         // Bytes processed in one iteration.
        MemAllocs uint64        // The total number of memory allocations; add in Go 1.1
        MemBytes  uint64        // The total number of bytes allocated; add in Go 1.1
    
        // Extra records aditional metrics reported by ReportMetric.
        Extra map[string]float64 // Go 1.13
    }
    
    package main
    
    import (
        "bytes"
        "fmt"
        "testing"
        "text/template"
    )
    
    func main() {
        benchmarkResult := 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")
                }
            })
        })
        fmt.Printf("%s\t%s\n", benchmarkResult.String(), benchmarkResult.MemString())
    }
    

    運行結(jié)果如下所示:

    $ go run main.go 
    30000000                46.0 ns/op            48 B/op          1 allocs/op
    

    具體函數(shù)定義及解釋可以參考golang BenchmarkResult

  • 子測試

    package codetest
    
    import (
      "fmt"
      "testing"
    )
    
    func TestFoo(t *testing.T) {
      // <setup code>
      t.Run("A=1", func(t *testing.T) { fmt.Println("A=1") })
      t.Run("A=2", func(t *testing.T) { fmt.Println("A=2") })
      t.Run("B=1", func(t *testing.T) { fmt.Println("B=1") })
      // <tear-down code>
    }
    
    func TestUoo(t *testing.T) {
      // <setup code>
      t.Run("U=1", func(t *testing.T) { fmt.Println("U=1") })
      t.Run("A=1", func(t *testing.T) { fmt.Println("A=1") })
      // <tear-down code>
    }
    

    -run-bench 命令行標(biāo)志的參數(shù)是與測試名稱相匹配的非固定的正則表達式。對于具有多個斜杠分隔元素(例如子測試)的測試,該參數(shù)本身是斜杠分隔的,其中表達式依次匹配每個名稱元素。因為它是非固定的,一個空的表達式匹配任何字符串。例如,使用 " 匹配 " 表示 " 其名稱包含 ":

    $ go test -run ''      # Run 所有測試。
    A=1
    A=2
    B=1
    U=1
    A=1
    PASS
    ok      _/Users/xxxxx/Workspace/test/src/codetest    0.004s
    
    $ go test -run Foo     # Run 匹配 "Foo" 的頂層測試,例如 "TestFooBar"。
    A=1
    A=2
    B=1
    PASS
    ok      _/Users/xxxxx/Workspace/test/src/codetest    0.005s
    
    $ go test -run Foo/A=  # 匹配頂層測試 "Foo",運行其匹配 "A=" 的子測試。
    A=1
    A=2
    PASS
    ok      _/Users/xxxxx/Workspace/test/src/codetest    0.004s
    
    $ go test -run /A=1    # 運行所有匹配 "A=1" 的子測試。
    A=1
    A=1
    PASS
    ok      _/Users/xxxxx/Workspace/test/src/codetest    0.004s
    

    子測試也可用于控制并行性。所有的子測試完成后,父測試才會完成。在這個例子中,所有的測試是相互并行運行的,當(dāng)然也只是彼此之間,不包括定義在其他頂層測試的子測試:

    func TestGroupedParallel(t *testing.T) {
      names := []string{"aa", "bb", "cc"}
    
      for _, name := range names {
          tName := name
          t.Run(tName, func(t *testing.T) {
              t.Parallel()
              fmt.Println(tName)
          })
      }
    }
    

    執(zhí)行命令go test -run .后輸出如下:

    $ go test -run .   
    aa
    cc
    bb
    PASS
    ok      _/Users/xxxxx/Workspace/test/src/codetest    0.004s
    

    在并行子測試完成之前,Run 方法不會返回,這提供了一種測試后清理的方法:

    func TestTeardownParallel(t *testing.T) {
        // This Run will not return until the parallel tests finish.
        t.Run("group", func(t *testing.T) {
            t.Run("Test1", parallelTest1)
            t.Run("Test2", parallelTest2)
            t.Run("Test3", parallelTest3)
        })
        // <tear-down code>
    }
    
    func parallelTest1(t *testing.T) {
      fmt.Println("Test1")
    }
    
    func parallelTest2(t *testing.T) {
      fmt.Println("Test2")
    }
    
    func parallelTest3(t *testing.T) {
      fmt.Println("Test3")
    }
    

常見問題解析

  1. 超時報錯*** Test killed with quit: ran too long (10m0s).

    出現(xiàn)此種類型的錯誤是因為golang test存在默認(rèn)的超時間為10m。

    -timeout d
            If a test binary runs longer than duration d, panic.
            If d is 0, the timeout is disabled.
            The default is 10 minutes (10m).
    

    若想要更長時間的運行測試,可以在命令中加入-timeout=60m參數(shù),參數(shù)接受s、m、h等級別的時間參數(shù)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容