Go Test测试简介

Go Test测试简介

Go语言拥有一套单元测试和性能测试系统,仅需要添加很少的代码就可以快速测试一段需求代码。go test 命令,会自动读取源码目录下面名为 *_test.go 的文件,生成并运行测试用的可执行文件并输出结果信息。

本文中测试用例来源于golang官方文档并且测试结果为golang 1.12.5下完成

测试类型

go test的测试类型分为以下三种

  • 单元测试,测试函数需要以Test为前缀,例如:

    func TestXxx(*testing.T)
    

    注意:Xxx 可以是任何字母数字字符串,但是第一个字母不能是小些字母。

    在这些函数中,使用 Error, Fail 或相关方法来发出失败信号。

    要编写一个新的测试套件,需要创建一个名称以 _test.go 结尾的文件,该文件包含 TestXxx 函数,如上所述。 将该文件放在与被测试的包相同的包中。该文件将被排除在正常的程序包之外,但在运行 “ go test ” 命令时将被包含。 有关详细信息,请运行 “ go help test ” 和 “ go help testflag ” 了解。

    如果有需要,可以调用 *T*B 的 Skip 方法,跳过该测试或基准测试:

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

    注意:testing.Short()判断是否-test.short flag是否被设置(后续示例会进行演示)

  • 基准测试,测试函数需要以Benchmark为前缀,例如:

      func BenchmarkXxx(*testing.B)
    

    通过 "go test" 命令,加上 -bench flag 来执行,多个基准测试按照顺序运行。

    基准测试函数如下所示:

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

    基准函数会运行目标代码 b.N 次。在基准执行期间,会调整 b.N 直到基准测试函数持续足够长的时间

    BenchmarkHelloWorld    1000000    216 ns/op
    

    意味着次基准测试函数在测试时间内循环执行了 1000000 次,每次循环花费 2216 纳秒 (ns)。

    注意:为了缩短基准测试,请使用-benchtime命令行标志设置基准测试运行时间,而不要使用testing.short()函数跳过部分基准测试。

  • 子测试

    单元测试和基准测试的Run方法允许定义子测试和子基准测试,而不必为每个子函数定义单独的功能。 这使得诸如table-driven测试以及creating hierarchical成为了可能。 它还提供了一种共享通用设置和拆卸代码的方法:

    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>
    }
    

    每个子测试和子基准测试都有一个唯一的名称:顶级测试的名称和传递给 Run 的名称的组合,以斜杠分隔,并具有用于消歧的可选尾随序列号。

code示例&执行解析

待测试的函数代码

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)
        }
    }
    

    执行go test .输出结果

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

    此结果表示测试通过。

    我们可以在文件fib_test.go新增一个测试函数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)
        }
    }
    

    执行go test .输出结果

    $ 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测试函数失败进而导致整体的单元测试失败。

    运行指定单元测试用例

    go test指定文件时默认执行文件内的所有测试用例。可以使用-run参数选择需要的测试用例单独执行,参考一下的代码。

    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")
    }
    

    当指定只执行TestCodeA开头的函数时,可以使用如下命令:

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

    输出结果如下:

    === 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覆盖测试

    当在测试中需要进行多case的覆盖测试,如果每次都以修改单元测试函数中的方式实现,这样无疑显得很笨拙。在此情况下,我们可以采用 Table-driven 的方式写测试。我们在lib_test.go中新增一个测试函数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 失败,并不会终止测试执行。如下所示:

    TestFibMore函数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
    
  • 基准测试

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

    运行指定基准测试用例

    若单元测试函数与基准测试函数在同一包下时或同一个文件时,可以通过在命令中添加 -run=none或者-run=^$来规避掉单元测试的运行

    运行测试命令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
    

    指定运基准测试运行时间

    我们还可以通过在命令中加入 -benchtime=60m来设置基准测试的运行时间。

    $ 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
    

    嵌套基准测试函数

    同时我们还可以通过创建一个基础函数并在此基础上衍生出更多基准测试的方法来进行多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应为系统的最大核心数

    内存统计

    在运行相关基准测试时,可以使用-benchmem参数查看基准测试的内存使用情况。

    $ 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
    

    从运行结果中看出在测试结果中增加了两列数据分别为xx B/opxx allocs/op,分别代表在-一次循环中分配的内存大小和一次循环中进行了多少次内存的分配。

    计时器的使用

    1. StartTimer()

      开始计时测试。在基准测试开始之前会自动调用此函数,但也可以用于在调用StopTimer之后恢复计时。

    2. StopTimer()

      停止计时测试。 这可用于在执行您不想测量的复杂初始化时暂停计时器。

    3. ResetTimer()

      将经过的基准时间和内存分配计数器归零,并删除用户报告的指标。 它不影响计时器是否正在运行。

    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函数的定义,推测只是代表耗时操作吧。

    并行执行基准测试

    如果基准测试需要在并行设置中测试性能,则可以使用 RunParallel 辅助函数 ; 这样的基准测试一般与 go test -cpu 标志一起使用。RunParallel 会创建出多个 goroutine,并将 b.N 分配给这些 goroutine 执行,其中 goroutine 数量的默认值为 GOMAXPROCS。如果想要增加goroutine的数量,可以调用函数SetParallelism将RunParallel使用的goroutine的数量设置为p * GOMAXPROCS。RunParallel 函数将在每个 goroutine 中执行,这个函数需要设置所有 goroutine 本地的状态,并迭代直到 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 一起,循环一共执行 b.N 次
                buf.Reset()
                templ.Execute(&buf, "World")
            }
        })
    }
    

    运行结果

    $ 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
    

    基准测试结果

    BenchmarkResult结构包含了基准测试运行后的结果。

    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())
    }
    

    运行结果如下所示:

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

    具体函数定义及解释可以参考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 命令行标志的参数是与测试名称相匹配的非固定的正则表达式。对于具有多个斜杠分隔元素(例如子测试)的测试,该参数本身是斜杠分隔的,其中表达式依次匹配每个名称元素。因为它是非固定的,一个空的表达式匹配任何字符串。例如,使用 " 匹配 " 表示 " 其名称包含 ":

    $ 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
    

    子测试也可用于控制并行性。所有的子测试完成后,父测试才会完成。在这个例子中,所有的测试是相互并行运行的,当然也只是彼此之间,不包括定义在其他顶层测试的子测试:

    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)
          })
      }
    }
    

    执行命令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).

    出现此种类型的错误是因为golang test存在默认的超时间为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参数,参数接受s、m、h等级别的时间参数

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