Golang单元测试快速上手(一) 基础

注:本文由linstring原创,博客地址:https://blog.csdn.net/lin_strong

转载请注明出处

第二部分链接:https://blog.csdn.net/lin_strong/article/details/109014085
第三部分链接:https://blog.csdn.net/lin_strong/article/details/109014623


前言

本系列文章分为三部分。
目标是带领一个有Golang基础的人快速入门Golang中的单元测试,明白基础概念,一些常用的技术、技巧和框架,写出合格的单测并能够hold住大部分的场景。

文章并不会涵盖所有的细节,而只写那些重要的,必知必会的知识。如想进一步深入请自行查阅相关资料。

什么是单元测试(Unit Test)

单元测试:指对软件中的最小可测试单元进行检查和验证。

单元:根据实际情况判定具体含义,如C语言中指一个函数,Java里指一个方法,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。

每个测试都应该是独立的小实验,不应该依赖于测试顺序。

每个测试应该要短小且聚焦。可以想做是一个小实验,这个实验通过时是静默的,失败时则制造噪音。

四步测试模式

一个单元测试分为以下几步:
Four-Phase Test Pattern(Gerard Meszaros’ book, xUnit Testing Patterns):

  1. Setup:建立测试的预环境(precondition)
  2. Exercise:做一些事情
  3. Verify:检查输出是否如期望的
  4. Cleanup:在测试后把系统还原为最初状态

FIRST原则

FIRST(from Book :Agile in a Flash)让测试更加高效。

  • F快速:测试是极其快的,快到开发者每次一进行小的更改就运行测试也不会打断工作。
  • I独立:测试是独立的。测试间不存在相互配置,测试的失败也是独立的。
  • R可重复:测试是可重复的;可重复就是自动化。循环执行测试会得到相同的结果。
  • S自验证:测试验证自己的输出,当通过时简单的说句“OK”,失败时精确地提供细节。
  • T适时的:测试是适时的。程序员适时地写它,即刚好在写产品代码前写,这避免了bug。

想了解更多单测知识的话请阅读专门的书籍。
以上是我从《Test-Driven Development for Embedded C》里学来的,很好的一本书,推荐阅读。
这是读书笔记:https://blog.csdn.net/lin_strong/article/details/84853138

Go中的测试约定

Go提供一个由 go test 命令和 testing package组成的轻量测试框架。约定:

  • XXXX_test.go形式的文件被认为是测试文件
  • 测试文件会被常规构建忽略,而不会被测试构建(使用go test命令)忽略
    • 实验发现:测试文件中的函数只对同个package内的测试文件可见
  • 一般把测试文件与被测试的文件放在一个package下,AAA.go的对应测试文件命名为AAA_test.go
    • 这也意味着测试文件能访问被测文件的unexport对象
  • 测试文件中所有形如以下的函数都会被认为是一个测试例程并在测试中被执行:
func TestXxx(*testing.T)
  • 注意:
    • Xxx第一字母大写
    • Xxx最好是要测试的方法名
    • A结构体的B方法的测试命名:TestA_B

示例仓库

https://download.csdn.net/download/lin_strong/12918620

一个简单的测试

假设我们有以下函数
add.go

package simpletest

func AddIfBPositive(a, b int) int {
   return a + b
}

add_test.go

package simpletest

import "testing"

func TestAddIfBPositive(t *testing.T) {
   // exercise
   got := AddIfBPositive(4, -1)
   // verify
   if got != 4 {
      t.Errorf("AddIfBPositive() = %v, want %v", got, 4)
   }
}

在目录(/package)下运行测试:

$ go test
--- FAIL: TestAddIfBPositive (0.00s)
    add_test.go:8: AddIfBPositive() = 3, want 4
FAIL
exit status 1
FAIL    go_test_demo/simpletest 0.881s

可以看到测试FAIL了,同时打印出了对应的错误信息。

将实现改对后再次运行测试:

package simpletest

func AddIfBPositive(a, b int) int {
   if b > 0 {
      return a + b
   }
   return a
}
$ go test
PASS
ok      go_test_demo/simpletest 0.227s

go test

go test工具用于执行Go测试

go test [build/test flags] [packages] [build/test flags & test binary flags]

For details

两个模式

local directory mode

  • 当不指定packages时,默认编译运行当前package的测试,最后列出package的测试总结行。
  • 这种模式下不会使用cache的测试结果
$ go test
PASS
ok      go_test_demo/simpletest 0.227s

package list mode

  • e.g.: ‘go test go_test_demo/simpletest’、 ‘go test ./…’ 和 ‘go test .’
  • 指定了packages时,编译并测试每一个列出的packages。
  • 通过时只会显示package的总结行,失败时会打印package的所有测试输出。(除非使用了-v或-bench选项)
  • 这种模式下,会使用cache的成功测试结果(会标识是cache结果),以避免没必要的测试

直接指定文件

还可以直接指定文件的方式来运行测试,但这种方式要把相关的文件都带上,不推荐

$ go test add_test.go add.go
ok      command-line-arguments  0.438s

常用命令/选项

  • 运行当前package的测试
go test
  • 运行当前路径及所有子路径下packages的测试:
go test ./...
  • 详细打印信息
$ go test -v
=== RUN   TestAddIfBPositive
--- PASS: TestAddIfBPositive (0.00s)
PASS
ok      go_test_demo/simpletest        0.171s
  • 更丰富的打印
go get -u github.com/rakyll/gotest

然后使用gotest替代所有的go test,输出的颜色上会更丰富些

  • 查看测试的覆盖率

-cover 选项

$ go test -cover
PASS
coverage: 66.7% of statements
ok      go_test_demo/simpletest        0.383s

升级版:结合go tool工具

go test ./... -coverprofile=cover.out && go tool cover -html=cover.out && rm cover.out

  • 运行选定测试(e.g. (一级)名字中包含Positive的测试)
$ go test -run=Positive
PASS
ok      go_test_demo/simpletest        0.605s

扩展阅读:The cover story

For other flags:https://golang.org/cmd/go/#hdr-Testing_flags

子测试

如为AddIfBPositive添加其他测试,可以写成:

package simpletest

import "testing"

func TestAddIfBPositiveWhenNegative(t *testing.T) {
   got := AddIfBPositive(4, -1)
   if got != 4 {
      t.Errorf("AddIfBPositive() = %v, want %v", got, 4)
   }
}

func TestAddIfBPositiveWhenZero(t *testing.T) {
   got := AddIfBPositive(-1, 0)
   if got != -1 {
      t.Errorf("AddIfBPositive() = %v, want %v", got, -1)
   }
}

func TestAddIfBPositiveWhenPositive(t *testing.T) {
   got := AddIfBPositive(0, 1)
   if got != 1 {
      t.Errorf("AddIfBPositive() = %v, want %v", got, 1)
   }
}

然后测试起来就成这样:

$ go test -v
=== RUN   TestAddIfBPositiveWhenNegative
--- PASS: TestAddIfBPositiveWhenNegative (0.00s)
=== RUN   TestAddIfBPositiveWhenZero
--- PASS: TestAddIfBPositiveWhenZero (0.00s)
=== RUN   TestAddIfBPositiveWhenPositive
--- PASS: TestAddIfBPositiveWhenPositive (0.00s)
PASS
ok      go_test_demo/simpletest        0.167s

使用Run方法创建子测试

但更好的方法可能是将其组织成子测试。
通过testing.T的Run方法,我们可以为当前测试创建子测试:

package subtest

import "testing"

func TestAddIfBPositive(t *testing.T) {
   t.Run("WhenNegative", func(t *testing.T) {
      got := AddIfBPositive(4, -1)
      if got != 4 {
         t.Errorf("AddIfBPositive() = %v, want %v", got, 4)
      }
   })
   t.Run("WhenZero", func(t *testing.T) {
      got := AddIfBPositive(-1, 0)
      if got != -1 {
         t.Errorf("AddIfBPositive() = %v, want %v", got, -1)
      }
   })
   t.Run("WhenPositive", func(t *testing.T) {
      got := AddIfBPositive(0, 1)
      if got != 1 {
         t.Errorf("AddIfBPositive() = %v, want %v", got, 1)
      }
   })
}

这样就获得了更好的可读性:

$ go test -v
=== RUN   TestAddIfBPositive
=== RUN   TestAddIfBPositive/WhenNegative
=== RUN   TestAddIfBPositive/WhenZero
=== RUN   TestAddIfBPositive/WhenPositive
--- PASS: TestAddIfBPositive (0.00s)
    --- PASS: TestAddIfBPositive/WhenNegative (0.00s)
    --- PASS: TestAddIfBPositive/WhenZero (0.00s)
    --- PASS: TestAddIfBPositive/WhenPositive (0.00s)
PASS
ok      go_test_demo/subtest  1.072s

同时,这也为给多个子测试共同进行Setup和Cleanup提供了方法:

func TestFoo(t *testing.T) {
    // setup code
    t.Run("A", func(t *testing.T) { ... })
    t.Run("B", func(t *testing.T) { ... })
    t.Run("C", func(t *testing.T) { ... })
    // cleanup code
}

分层组织的测试

  • 子测试可以不断嵌套,每嵌套一次多一层
  • 同一个package下测试函数都是从属于package的子测试
  • 测试间按照类似 文件路径/树 的方式组织。
  • 任一子测试的失败导致父测试也失败
package subtest

import "testing"

func TestAddIfBPositive(t *testing.T) {
   t.Run("WhenNegative", func(t *testing.T) {
      got := AddIfBPositive(4, -1)
      if got != 4 {
         t.Errorf("AddIfBPositive() = %v, want %v", got, 4)
      }
   })
   t.Run("WhenZero", func(t *testing.T) {
      got := AddIfBPositive(-1, 0)
      if got != -1 {
         t.Errorf("AddIfBPositive() = %v, want %v", got, -1)
      }
      t.Run("FailOnPurpose", func(t *testing.T) {
         t.Fail()
      })
   })
   t.Run("WhenPositive", func(t *testing.T) {
      got := AddIfBPositive(0, 1)
      if got != 1 {
         t.Errorf("AddIfBPositive() = %v, want %v", got, 1)
      }
   })
}
$ go test -v
=== RUN   TestAddIfBPositive
=== RUN   TestAddIfBPositive/WhenNegative
=== RUN   TestAddIfBPositive/WhenZero
=== RUN   TestAddIfBPositive/WhenZero/FailOnPurpose
=== RUN   TestAddIfBPositive/WhenPositive
--- FAIL: TestAddIfBPositive (0.00s)
    --- PASS: TestAddIfBPositive/WhenNegative (0.00s)
    --- FAIL: TestAddIfBPositive/WhenZero (0.00s)
        --- FAIL: TestAddIfBPositive/WhenZero/FailOnPurpose (0.00s)
    --- PASS: TestAddIfBPositive/WhenPositive (0.00s)
FAIL
exit status 1
FAIL    go_test_demo/subtest  0.519s
  • 每个测试都有唯一的名字,从顶层的名字往下直到自己的名字,每层名字用“/”连接到一起。可能最后会还会加一个序号来进行区分。

选择特定测试

go test工具的run参数用于指定要运行的测试

使用分层的正则表达式匹配,用“/”分隔层级

比如:

  • 运行一级名包含Add的测试
$ go test -v -run=Add
=== RUN   TestAddIfBPositive
=== RUN   TestAddIfBPositive/WhenNegative
=== RUN   TestAddIfBPositive/WhenZero
=== RUN   TestAddIfBPositive/WhenZero/FailOnPurpose
=== RUN   TestAddIfBPositive/WhenPositive
--- FAIL: TestAddIfBPositive (0.00s)
    --- PASS: TestAddIfBPositive/WhenNegative (0.00s)
    --- FAIL: TestAddIfBPositive/WhenZero (0.00s)
        --- FAIL: TestAddIfBPositive/WhenZero/FailOnPurpose (0.00s)
    --- PASS: TestAddIfBPositive/WhenPositive (0.00s)
FAIL
exit status 1
FAIL    go_test_demo/subtest  0.512s
  • 运行一级名包含AddIfB且二级包含tive的测试
$ go test -v -run=AddIfB/tive
=== RUN   TestAddIfBPositive
=== RUN   TestAddIfBPositive/WhenNegative
=== RUN   TestAddIfBPositive/WhenPositive
--- PASS: TestAddIfBPositive (0.00s)
    --- PASS: TestAddIfBPositive/WhenNegative (0.00s)
    --- PASS: TestAddIfBPositive/WhenPositive (0.00s)
PASS
ok      go_test_demo/subtest  1.065s

可以看到WhenZero子测试被过滤掉了

  • 运行二级名包含tive的测试(注意,不包含二级测试的测试也会被匹配到)
$ go test -v -run=/tive
  • 运行二级名以When开头然后跟着N或Z的测试
$ go test -run=/"^When[N|Z]" -v
=== RUN   TestAddIfBPositive
=== RUN   TestAddIfBPositive/WhenNegative
=== RUN   TestAddIfBPositive/WhenZero
=== RUN   TestAddIfBPositive/WhenZero/FailOnPurpose
--- FAIL: TestAddIfBPositive (0.00s)
    --- PASS: TestAddIfBPositive/WhenNegative (0.00s)
    --- FAIL: TestAddIfBPositive/WhenZero (0.00s)
        --- FAIL: TestAddIfBPositive/WhenZero/FailOnPurpose (0.00s)
FAIL
exit status 1
FAIL    go_test_demo/subtest  0.321s

指定特定测试可以在编写/调试某个测试时节省大量时间

表驱动的测试

很多时候测试用例有大量冗余,只是几个入参和出参发生变化。这时候很适合使用表驱动。

如上面的例子用表驱动写成:

package tabledriventest

import "testing"

func TestAddIfBPositive(t *testing.T) {
   type args struct {
      a int
      b int
   }
   tests := []struct {
      name string
      args args
      want int
   }{
      {"WhenNegative", args{a: 4, b: -1}, 4},
      {"WhenZero", args{a: -1, b: 0}, -1},
      {"WhenPositive", args{a: 0, b: 1}, 1},
   }
   for _, tt := range tests {
      t.Run(tt.name, func(t *testing.T) {
         if got := AddIfBPositive(tt.args.a, tt.args.b); got != tt.want {
            t.Errorf("AddIfBPositive() = %v, want %v", got, tt.want)
         }
      })
   }
}

这样,就可以轻松地添加更多咱们认为必要的测试用例了。
输出和前面没加FailOnPurpose时一致。

官方库给了很多很棒的表驱动测试的例子:
https://golang.org/src/fmt/fmt_test.go

每个测试都是个goroutine

  • Run会为子测试创建一个goroutine来实际运行,这样就可以通过goroutine退出,或者goroutine间相互阻塞来方便地控制整个流程。
  • Run默认会阻塞到子测试运行完毕,所以效果上好像是直接调用了函数

并行测试

  • 子测试可以通过在进入测试时立即调用Parallel来和同个父测试下其他其他也调用了这个方法的子测试并行运行。就像这样
……
for _, tt := range tests {
   t.Run(tt.name, func(t *testing.T) {
      t.Parallel()
      if got := AddIfBPositive(tt.args.a, tt.args.b); got != tt.want {
         t.Errorf("AddIfBPositive() = %v, want %v", got, tt.want)
      }
   })
}
  • 调用Parallel后子测试暂时交还控制权,父测试得以先执行其他测试
  • 父测试在做完其他非并行子测试后,退出前,会取消所有并行子测试的阻塞,并在所有并行子测试完成后才退出。
$ go test -v
=== RUN   TestAddIfBPositive
=== RUN   TestAddIfBPositive/WhenNegative
=== PAUSE TestAddIfBPositive/WhenNegative
=== RUN   TestAddIfBPositive/WhenZero
=== PAUSE TestAddIfBPositive/WhenZero
=== RUN   TestAddIfBPositive/WhenPositive
=== PAUSE TestAddIfBPositive/WhenPositive
=== CONT  TestAddIfBPositive/WhenNegative
=== CONT  TestAddIfBPositive/WhenPositive
=== CONT  TestAddIfBPositive/WhenZero
--- PASS: TestAddIfBPositive (0.00s)
    --- PASS: TestAddIfBPositive/WhenNegative (0.00s)
    --- PASS: TestAddIfBPositive/WhenPositive (0.00s)
    --- PASS: TestAddIfBPositive/WhenZero (0.00s)
PASS
ok      go_test_demo/tabledriventest   0.776s

GoLand自动生成测试

其实上面的表驱动例子的框架是GoLand自动生成的。
通过在要进行测试的函数上 Shift+Comand+T -> Test for function

IDE会自动为此函数生成测试用例框架:

func TestAddIfBPositive(t *testing.T) {
   type args struct {
      a int
      b int
   }
   tests := []struct {
      name string
      args args
      want int
   }{
      // TODO: Add test cases.
   }
   for _, tt := range tests {
      t.Run(tt.name, func(t *testing.T) {
         if got := AddIfBPositive(tt.args.a, tt.args.b); got != tt.want {
            t.Errorf("AddIfBPositive() = %v, want %v", got, tt.want)
         }
      })
   }
}

如果没有测试文件的话也会自动创建。

然后就可以在其上修改来实现测试用例。

主测试

如果测试文件包含函数:

func TestMain(m *testing.M)

则生成的测试不会直接运行各测试用例,而是去调用TestMain(m)。

TestMain运行于主goroutine,可以在调用m.Run前后进行setup和cleanup。m.Run将返回退出码,退出码一般是传给os.Exit。在TestMain退出后,测试会自动把m.Run的结果传给os.Exit。

在TestMain被调用时,flag.Parse还没有运行。如果TestMain依赖于(包括testing package里的那些)命令行标志位,那就应该直接调用flag.Parse。当测试函数运行时,命令行标志位总是已经转化好的。

示例:

package maintest

import (
   "os"
   "testing"
)
var set = false

func TestMain(m *testing.M) {
   // call flag.Parse() here if TestMain uses flags
   // setup code
   set = true
   os.Exit(m.Run())
   // cleanup code
   set = false
}

func TestA(t *testing.T) {
   t.Errorf("set = %v", set)
}

func TestB(t *testing.T) {
   t.Errorf("set = %v", set)
}
$ go test   
--- FAIL: TestA (0.00s)
    main_test.go:19: set = true
--- FAIL: TestB (0.00s)
    main_test.go:23: set = true
FAIL
exit status 1
FAIL    go_test_demo/maintest  0.411s

另:由于测试函数在一个package内的所有测试文件间是相互可见的,道理上一个package只能有一个TestMain;当然,要是是直接指定测试文件的写法,各自放一个TestMain是可以各自完成测试。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值