扫码订阅《 》或入驻星球,即可阅读文章!

GOLANG ROADMAP

阅读模式

  • 沉浸
  • 自动
  • 日常
首页
Go友会
  • 城市
  • 校园
Go学院
  • Go小课
  • Go小考
  • Go实战
  • 精品课
Go求职
  • 求职辅导🔥
  • Offer收割社群
  • 企业题库
  • 面试宝典
Go宝典
  • 在线宝典
  • B站精选
  • 推荐图书
  • 每日博文
Go仓库
实验区
  • Go周边
  • Go下载
  • Go月刊
消息
更多
  • 用户中心

    • 我的信息
    • 推广返利
  • 玩转星球

    • 星球介绍
    • 角色体系
    • 星主权益
  • 支持与服务

    • 联系星主
    • 成长记录
    • 常见问题
    • 吐槽专区
  • 合作交流

    • 渠道合作
    • 课程入驻
    • 友情链接
author-avatar

GOLANG ROADMAP


首页
Go友会
  • 城市
  • 校园
Go学院
  • Go小课
  • Go小考
  • Go实战
  • 精品课
Go求职
  • 求职辅导🔥
  • Offer收割社群
  • 企业题库
  • 面试宝典
Go宝典
  • 在线宝典
  • B站精选
  • 推荐图书
  • 每日博文
Go仓库
实验区
  • Go周边
  • Go下载
  • Go月刊
消息
更多
  • 用户中心

    • 我的信息
    • 推广返利
  • 玩转星球

    • 星球介绍
    • 角色体系
    • 星主权益
  • 支持与服务

    • 联系星主
    • 成长记录
    • 常见问题
    • 吐槽专区
  • 合作交流

    • 渠道合作
    • 课程入驻
    • 友情链接

扫码订阅《 》或入驻星球,即可阅读文章!

测试


李二狗

优秀的代码习惯一定是伴随着单元测试的,这也是go语言设计的哲学;

国外的很多公司很多优秀的程序员都比较重视TDD,但是在国内十分少见;(TDD:测试驱动开发(test driven devlopment))

无论如何,学习并使用golang的单元测试,不是浪费时间,而是让你的代码更加优雅健硕!

# 测试文件

文件名以_test.go为后缀的文件均为测试代码,都会被go test测试捕捉,不会被go build编译;

# 测试函数

测试文件中的三种函数类型:

  • 单元测试函数:函数名前缀Test;测试程序的逻辑
  • 基准函数:函数名前缀Benchmark;测试函数的性能
  • 示例函数:函数名前缀Example;会出现在godoc中的,为文档提供示例文档

# 测试命令

Go语言中的测试依赖go test命令;在此命令下添加各种不同的参数以实现不同目的的测试;后面会一一介绍;

go test命令会遍历所有的*_test.go文件中符合上述命名规则的测试函数;

然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件;


接下来分别介绍单元测试函数、基准函数、示例函数:

# 单元测试函数

  • 单元测试函数的格式:

    1. func TestName(t *testing.T) {}
    2. 函数的名字必须以Test开头,可选的后缀名必须以大写字母开头
    3. 每个测试函数必须导入testing包;关于testing包中的方法可以去看一下源码;
    4. 参数t用于报告测试失败和附加的日志信息
  • 一个简单的测试函数示例:将输出的结果与预期的结果进行比较

    1. 创建业务函数

      // 文件split/split.go:定义一个split的包,包中定义了一个Split函数
      package split
      
      import "strings"
      
      func Split(s, sep string) (result []string) {
      i := strings.Index(s, sep)
      for i > -1 {
      result = append(result, s[:i])
      s = s[i+1:]
      i = strings.Index(s, sep)
      }
      result = append(result, s)
      return
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
    2. 创建测试文件

      // 文件split/split_test.go:创建一个split_test.go的测试文件
      package split
      
      import (
      "reflect"
      "testing"
      )
      
      // 单元测试函数
      // 测试函数名必须以Test开头,必须接收一个*testing.T类型参数
      // 1. 直接调用业务函数
      // 2. 定义期望结果
      // 3. 比较实际结果和期望结果
      func TestSplit(t *testing.T) {
      got := Split("a:b:c", ":")      // 调用程序并返回程序结果
      want := []string{"a", "b", "c"} // 期望的结果
      
      if !reflect.DeepEqual(want, got) { // 因为slice不能直接比较,借助反射包中的方法比较
      t.Errorf("expected:%v, got:%v", want, got) // 如果测试失败输出错误提示
      }
      }
      
      // 提供一个失败的单元测试
      func TestSplitFail(t *testing.T) {
      got := Split("abcd", "bc")
      want := []string{"a", "d"}
      
      if !reflect.DeepEqual(want, got) {
      t.Errorf("expected:%v, got:%v", want, got)
      }
      }
      
      // 基准测试函数
      func BenchmarkSplit(b *testing.B) {
      
      }
      
      // 示例函数
      func ExampleSplit() {
      
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
    3. 执行测试命令

      进入split目录下,直接运行 go test 命令即可;

      如果运行go test -v的话,可以看到更详细的输出结果:知道哪个测试函数没有通过,错在哪里

      === RUN   TestSplit
      --- PASS: TestSplit (0.00s)
      === RUN   TestSplitFail
         split_test.go:28: expected:[a d], got:[a cd]
      --- FAIL: TestSplitFail (0.00s)
      FAIL
      exit status 1
      FAIL    gotest/split    0.001s
      
      1
      2
      3
      4
      5
      6
      7
      8
  • 其他go test命令

    go test -run=?:run对应一个正则表达式,只有函数名匹配上的测试函数才会被go test命令执行;

    // 比如以上代码执行命令:go test -v -run=Fail
    // 表示本次只运行 能正则匹配到Fail的 测试函数
    === RUN   TestSplitFail
       split_test.go:28: expected:[a d], got:[a cd]
    --- FAIL: TestSplitFail (0.00s)
    FAIL
    exit status 1
    FAIL    gotest/split    0.001s
    
    1
    2
    3
    4
    5
    6
    7
    8

    go test -short:跳过测试函数中包含testing.Short()函数的测试函数;一般用于跳过执行起来太耗时的测试函数;比如:

    // 修改以上示例代码中的TestSplitFail函数如下
    func TestSplitFail(t *testing.T) {
    if testing.Short() {
    t.Skip("short模式下会跳过该测试用例")
    }
    got := Split("abcd", "bc") // 调用程序并返回程序结果
    want := []string{"a", "d"} // 期望的结果
    
    if !reflect.DeepEqual(want, got) { // 因为slice不能直接比较,借助反射包中的方法比较
    t.Errorf("expected:%v, got:%v", want, got) // 如果测试失败输出错误提示
    }
    }
    
    // 然后执行命令`go test -v -short`打印如下结果:
    === RUN   TestSplit
    --- PASS: TestSplit (0.00s)
    === RUN   TestSplitFail
       split_test.go:25: short模式下会跳过该测试用例
    --- SKIP: TestSplitFail (0.00s)
    PASS
    ok      gotest/split    0.002s
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    go test -cover测试覆盖率:覆盖率是指测试代码覆盖的业务代码的占比;

    go test -cover -coverprofile=c.out将覆盖率相关的信息输出到当前文件夹下面的c.out文件中;

    再然后执行go tool cover -html=c.out,使用cover工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成一个HTML报告;

  • 子测试:对多组测试用例能够清晰准确的进行错误定位

    Go1.7+中新增了子测试,我们可以按照如下方式使用t.Run执行子测试:

    func TestSplit(t *testing.T) {
    type test struct {
    input string
    sep   string
    want []string
    }
    // 定义多组测试用例
    tests := map[string]test{
    "simple":     {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
    "wrong sep":   {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
    "more sep":   {input: "abcd", sep: "bc", want: []string{"a", "d"}},
    "leading sep": {input: "沙河有沙又有河", sep: "沙", want: []string{"河有", "又有河"}},
    }
    
    // t.Run就是子测试
    for name, tc := range tests {
    t.Run(name, func(t *testing.T) { // 使用t.Run()执行子测试
    got := Split(tc.input, tc.sep)
    if !reflect.DeepEqual(got, tc.want) {
    t.Errorf("expected:%#v, got:%#v", tc.want, got)
    }
    })
    }
    }
    
    // 执行 go test -v
    === RUN   TestSplit
    === RUN   TestSplit/simple
    === RUN   TestSplit/wrong_sep
    === RUN   TestSplit/more_sep
       split_test.go:34: expected:[]string{"a", "d"}, got:[]string{"a", "cd"}
    === RUN   TestSplit/leading_sep
       split_test.go:34: expected:[]string{"河有", "又有河"}, got:[]string{"", "\xb2\x99河有", "\xb2\x99又有河"}
    --- FAIL: TestSplit (0.00s)
       --- PASS: TestSplit/simple (0.00s)
       --- PASS: TestSplit/wrong_sep (0.00s)
       --- FAIL: TestSplit/more_sep (0.00s)
       --- FAIL: TestSplit/leading_sep (0.00s)
    FAIL
    exit status 1
    FAIL    gotest/split    0.002s
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41

# 基准函数

在一定的工作负载之下检测程序性能;

基本格式如:func BenchmarkName(b *testing.B){}

  • 以Benchmark为前缀,后面跟着首字母大写

  • 必须要有testing.B类型的参数;

  • 基准测试必须要执行b.N次,这样的测试才有对照性,b.N的值是系统根据实际情况去调整的(见后面使用示例)

  • 关于testing.B的方法可以参考源码

  • 一个简单的基准函数的示例:

    1. 创建测试函数(继续接着上面的示例代码)

      func BenchmarkSplit(b *testing.B) {
         // 注意b.N
      for i := 0; i < b.N; i++ {
      Split("沙河有沙又有河", "沙")
      }
      }
      
      1
      2
      3
      4
      5
      6
    2. 执行测试命令-bench=$正则匹配

      // go test -bench=Split
      goos: windows
      goarch: amd64
      pkg: go-test/split
      cpu: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
      BenchmarkSplit-4         4017925               311.9 ns/op
      PASS
      ok      go-test/split      1.976s
      
      1
      2
      3
      4
      5
      6
      7
      8

      以上输出了电脑的一些信息;

      其中BenchmarkSplit-4中的4表示GOMAXPROCS的值;

      4017925表示调用该函数的次数;

      311.9 ns/op调用该函数的平均耗时;

      // `go test -bench=Split -benchmem`增加了内存分配的统计数据
      goos: windows
      goarch: amd64
      pkg: go-test/split
      cpu: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
      BenchmarkSplit-4         3995856               300.9 ns/op           112 B/op          3 allocs/op
      PASS
      ok      go-test/split      1.890s
      
      1
      2
      3
      4
      5
      6
      7
      8

      其中112 B/op表示每次操作内存分配了112字节;

      3 allocs/op表示每次操作进行了3次内存分配;

  • 性能比较函数

    1. 基准函数只能给出绝对耗时;实际中我们可能想知道不同操作的相对耗时;
    2. 性能比较函数通常是一个带有参数的函数,被多个不同的Benchmark函数传入不同的值来调用;
  • 一个简单的基准测试中的性能比较函数示例:

    1. 创建一个fib目录,并新建一个fib.go文件

      package fib
      
      // Fib 是一个计算第n个斐波那契数的函数
      func Fib(n int) int {
      if n < 2 {
      return n
      }
      return Fib(n-1) + Fib(n-2)
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
    2. 同目录下创建一个测试函数文件fib_test.go

      package fib
      
      import (
      "testing"
      )
      
      func benchmarkFib(b *testing.B, n int) {
      for i := 0; i < b.N; i++ {
      Fib(n)
      }
      }
      
      func BenchmarkFib1(b *testing.B) { benchmarkFib(b, 1) }
      func BenchmarkFib2(b *testing.B) { benchmarkFib(b, 2) }
      func BenchmarkFib3(b *testing.B) { benchmarkFib(b, 3) }
      func BenchmarkFib10(b *testing.B) { benchmarkFib(b, 10) }
      func BenchmarkFib20(b *testing.B) { benchmarkFib(b, 20) }
      func BenchmarkFib40(b *testing.B) { benchmarkFib(b, 40) }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
    3. 执行测试命令

      // go test -bench=Fib
      goos: windows
      goarch: amd64
      pkg: go-test/fib
      cpu: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
      BenchmarkFib1-4         470478618                2.393 ns/op
      BenchmarkFib2-4         178773399                6.737 ns/op
      BenchmarkFib3-4         100000000               12.60 ns/op
      BenchmarkFib10-4         3025942               421.8 ns/op
      BenchmarkFib20-4           24792             55344 ns/op
      BenchmarkFib40-4               2         724560000 ns/op
      PASS
      ok      go-interview/fib        11.675s
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13

      BenchmarkFib40-4 2 724560000 ns/op:第一个是对应的比较函数;第二个是执行的次数;第三个是执行的平均时间;

      由此可见fib函数入参值越大,函数执行的效率越低;

  • 基准测试命令除了以上这些外还有一些其他的参数做其他用处:比如这些参数benchtime,ResetTimer,RunParallel,SetParallelism,cpu,setup,teardown,TestMain,可以了解一下

# 示例函数

示例函数能够作为文档直接使用,例如基于web的godoc中能把示例函数与对应的函数或包相关联;

因为我用的很少,所以大家感兴趣的可以自己去了解一下;我工作中常用的就是通过golang的单元测试来帮助我检查代码的问题和优化代码的性能;


对测试内容的补充,目录如下:

# 表格驱动测试

表格测试是一种编写更清晰的测试函数的方法;

顾名思义,表格驱动测试,就是指通过表格列举的方式来实现测试用例,表格中包含输入和预期输出,以及其他信息;这种方式是我们对测试的逻辑和思路更加清晰;

官方表格驱动测试的案例:

// fmt包中有如下一段测试代码:
// 定义测试的表格,包含了in输入字段和out期待输出字段
// 并且定义了该表格中的测试用例
// 然后使用t.Run的方式对每个用例进行测试
var flagtests = []struct {
    in  string
    out string
}{
    {"%a", "[%a]"},
    {"%-a", "[%-a]"},
    {"%+a", "[%+a]"},
    {"%#a", "[%#a]"},
    {"% a", "[% a]"},
    {"%0a", "[%0a]"},
    {"%1.2a", "[%1.2a]"},
    {"%-1.2a", "[%-1.2a]"},
    {"%+1.2a", "[%+1.2a]"},
    {"%-+1.2a", "[%+-1.2a]"},
    {"%-+1.2abc", "[%+-1.2a]bc"},
    {"%-1.2abc", "[%-1.2a]bc"},
}
func TestFlagParser(t *testing.T) {
    var flagprinter flagPrinter
    for _, tt := range flagtests {
        t.Run(tt.in, func(t *testing.T) {
            s := Sprintf(tt.in, &flagprinter)
            if s != tt.out {
                t.Errorf("got %q, want %q", s, tt.out)
            }
        })
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

如果测试用例我们例举了非常多的话,我们希望测试用例可以并行执行,本身每个测试用例之间就是互不干扰的,因此上述代码可如下优化:

func TestFlagParser(t *testing.T) {
    var flagprinter flagPrinter
    for _, tt := range flagtests {
        ft := tt // 1. 重新声明变量,避免多个goroutine中使用了相同的变量
        t.Run(ft.in, func(t *testing.T) {
             t.Parallel()  // 2. 使用t.Parallel表示每个子测试之间能够彼此并行运行
            s := Sprintf(ft.in, &flagprinter)
            if s != ft.out {
                t.Errorf("got %q, want %q", s, ft.out)
            }
        })
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

如上,既是表格驱动测试的用法;先定义表格以及测试用例,然后再通过t.Run子测试方式遍历表格;

# 自动生成表格驱动测试的代码gotests

gotests是一种自动生成表格驱动测试代码的工具;

使用案例:

  1. 比如在前面我们创建了split.go的业务文件;

  2. 安装gotests工具:$ go get -u github.com/cweill/gotests/...

  3. 执行命令:gotests -all -w split.go;关于gotests的命令参数,大家可以去官网学习一下;

    • -all:生成所有的测试函数和方法
    • -w:输出测试结果到文件而不是控制台
  4. 生成的测试代码的格式如下,我们需要在todo的位置添加我们的测试逻辑即可:

    package base_demo
    
    import (
     "reflect"
     "testing"
    )
    
    func TestSplit(t *testing.T) {
     type args struct {
         s   string
         sep string
     }
     tests := []struct {
         name       string
         args       args
         wantResult []string
     }{
         // TODO: Add test cases.
     }
     for _, tt := range tests {
         t.Run(tt.name, func(t *testing.T) {
             if gotResult := Split(tt.args.s, tt.args.sep); !reflect.DeepEqual(gotResult, tt.wantResult) {
                 t.Errorf("Split() = %v, want %v", gotResult, tt.wantResult)
             }
         })
     }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27

# go测试工具包--testfy

安装go get github.com/stretchr/testify;提供了更优雅的,灵活的,可mock的等等工具;

常用:testify/assert或testify/require或testfy/mock

示例:

单元测试的时候,经常需要用到断言来检验测试结果,但是golang官方没有提供断言语法,导致我们可能会使用大量的ifelse语句;

testfy/assert为我们提供了很多常用的断言函数,让我们的测试代码实现的更加优雅;

比如在前面我们检验TestSplit结果的方式如下:

if !reflect.DeepEqual(want, got) {
    t.Errorf("expected:%v, got:%v", want, got)
}
1
2
3

如果我们使用testfy/assert的话,就可以如下简化:

// t是testing.T
assert.Equal(t, want, got)  // 使用assert提供的断言函数;

//或者如下使用方式,先创建assert对象:
assert := assert.New(t)

assert.Equal(123, 123, "they should be equal")//是否相等测试
assert.NotEqual(123, 456, "they should not be equal")//是否不等测试
assert.Nil(object)//是否nil测试
if assert.NotNil(object) {
    assert.Equal("Something", object.Value)
}
1
2
3
4
5
6
7
8
9
10
11
12

testfy中除了assert工具以外,还有常用的是testfy/require工具,以及还提供了mock和http的工具,大家可以去官网了解一下;

什么是mock呢?比如我们的测试中有一个步骤是向用户成功发送邮件,事实上我们需要用户确认邮件后,才认为该邮件用户已确认;但实际测试中,我们不可能真的给用户发送邮件,或者说我们不可能每次测试都真的发送邮件(假如不是邮件而是短信的话,每次测试可都是需要花钱的),因此mock就可以模拟用户确认短信的行为,即模拟功能;

mock技术可用于各种不同的系统,例如模拟数据库查询或者是与其他API的交互等等,非常实用;

文章来源: 一文搞定golang单元测试 (opens new window)

  • 测试文件
  • 测试函数
  • 测试命令
  • 单元测试函数
  • 基准函数
  • 示例函数
  • 表格驱动测试
  • 自动生成表格驱动测试的代码gotests
  • go测试工具包--testfy