这是Go单测从入门到放弃系列教程的第0篇,主要讲解在Go语言中如何做单元测试以及介绍了表格驱动测试、回归测试,并且介绍了常用的断言工具。

Go语言测试

go test工具

Go语言中的测试依赖go test命令。编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具。

go test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。

*_test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。

类型 格式 作用
测试函数 函数名前缀为Test 测试程序的一些逻辑行为是否正确
基准函数 函数名前缀为Benchmark 测试函数的性能
示例函数 函数名前缀为Example 为文档提供示例文档

go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。

单元测试函数

格式

每个测试函数必须导入testing包,测试函数的基本格式(签名)如下:

func TestName(t *testing.T){// ...
}

测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头,举几个例子:

func TestAdd(t *testing.T){ ... }
func TestSum(t *testing.T){ ... }
func TestLog(t *testing.T){ ... }

其中参数t用于报告测试失败和附加的日志信息。testing.T的拥有的方法如下:

func (c *T) Cleanup(func())
func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
func (c *T) Helper()
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool
func (c *T) TempDir() string

单元测试示例

就像细胞是构成我们身体的基本单位,一个软件程序也是由很多单元组件构成的。单元组件可以是函数、结构体、方法和最终用户可能依赖的任意东西。总之我们需要确保这些组件是能够正常运行的。单元测试是一些利用各种方法测试单元组件的程序,它会将结果与预期输出进行比较。

接下来,我们在base_demo包中定义了一个Split函数,具体实现如下:

// base_demo/split.gopackage base_demoimport "strings"// Split 把字符串s按照给定的分隔符sep进行分割返回字符串切片
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
}

在当前目录下,我们创建一个split_test.go的测试文件,并定义一个测试函数如下:

// split/split_test.gopackage splitimport ("reflect""testing"
)func TestSplit(t *testing.T) { // 测试函数名必须以Test开头,必须接收一个*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) // 测试失败输出错误提示}
}

此时split这个包中的文件如下:

❯ ls -l
total 16
-rw-r--r--  1 liwenzhou  staff  408  4 29 15:50 split.go
-rw-r--r--  1 liwenzhou  staff  466  4 29 16:04 split_test.go

在当前路径下执行go test命令,可以看到输出结果如下:

❯ go test
PASS
ok      golang-unit-test-demo/base_demo       0.005s

go test -v

一个测试用例有点单薄,我们再编写一个测试使用多个字符切割字符串的例子,在split_test.go中添加如下测试函数:

func TestSplitWithComplexSep(t *testing.T) {got := Split("abcd", "bc")want := []string{"a", "d"}if !reflect.DeepEqual(want, got) {t.Errorf("expected:%v, got:%v", want, got)}
}

现在我们有多个测试用例了,为了能更好的在输出结果中看到每个测试用例的执行情况,我们可以为go test命令添加-v参数,让它输出完整的测试结果。

❯ go test -v
=== RUN   TestSplit
--- PASS: TestSplit (0.00s)
=== RUN   TestSplitWithComplexSepsplit_test.go:20: expected:[a d], got:[a cd]
--- FAIL: TestSplitWithComplexSep (0.00s)
FAIL
exit status 1
FAIL    golang-unit-test-demo/base_demo 0.009s

从上面的输出结果我们能清楚的看到是TestSplitWithComplexSep这个测试用例没有测试通过。

go test -run

单元测试的结果表明split函数的实现并不可靠,没有考虑到传入的sep参数是多个字符的情况,下面我们来修复下这个Bug:

package base_demoimport "strings"// Split 把字符串s按照给定的分隔符sep进行分割返回字符串切片
func Split(s, sep string) (result []string) {i := strings.Index(s, sep)for i > -1 {result = append(result, s[:i])s = s[i+len(sep):] // 这里使用len(sep)获取sep的长度i = strings.Index(s, sep)}result = append(result, s)return
}

在执行go test命令的时候可以添加-run参数,它对应一个正则表达式,只有函数名匹配上的测试函数才会被go test命令执行。

例如通过给go test添加-run=Sep参数来告诉它本次测试只运行TestSplitWithComplexSep这个测试用例:

❯ go test -run=Sep -v
=== RUN   TestSplitWithComplexSep
--- PASS: TestSplitWithComplexSep (0.00s)
PASS
ok      golang-unit-test-demo/base_demo 0.010s

最终的测试结果表情我们成功修复了之前的Bug。

回归测试

我们修改了代码之后仅仅执行那些失败的测试用例或新引入的测试用例是错误且危险的,正确的做法应该是完整运行所有的测试用例,保证不会因为修改代码而引入新的问题。

❯ go test -v
=== RUN   TestSplit
--- PASS: TestSplit (0.00s)
=== RUN   TestSplitWithComplexSep
--- PASS: TestSplitWithComplexSep (0.00s)
PASS
ok      golang-unit-test-demo/base_demo 0.011s

测试结果表明我们的单元测试全部通过。

通过这个示例我们可以看到,有了单元测试就能够在代码改动后快速进行回归测试,极大地提高开发效率并保证代码的质量。

跳过某些测试用例

为了节省时间支持在单元测试时跳过某些耗时的测试用例。

func TestTimeConsuming(t *testing.T) {if testing.Short() {t.Skip("short模式下会跳过该测试用例")}...
}

当执行go test -short时就不会执行上面的TestTimeConsuming测试用例。

子测试

在上面的示例中我们为每一个测试数据编写了一个测试函数,而通常单元测试中需要多组测试数据保证测试的效果。Go1.7+ 中新增了子测试,支持在测试函数中使用t.Run执行一组测试用例,这样就不需要为不同的测试数据定义多个测试函数了。

func TestXXX(t *testing.T){t.Run("case1", func(t *testing.T){...})t.Run("case2", func(t *testing.T){...})t.Run("case3", func(t *testing.T){...})
}

表格驱动测试

介绍

编写好的测试并非易事,但在许多情况下,表格驱动测试可以涵盖很多方面:表格里的每一个条目都是一个完整的测试用例,包含输入和预期结果,有时还包含测试名称等附加信息,以使测试输出易于阅读。

使用表格驱动测试能够很方便的维护多个测试用例,避免在编写单元测试时频繁的复制粘贴。

表格驱动测试的步骤通常是定义一个测试用例表格,然后遍历表格,并使用t.Run对每个条目执行必要的测试。

表格驱动测试不是工具、包或其他任何东西,它只是编写更清晰测试的一种方式和视角。

示例

官方标准库中有很多表格驱动测试的示例,例如fmt包中的测试代码:

var flagtests = []struct {in  stringout 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 flagPrinterfor _, 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)}})}
}

通常表格是匿名结构体数组切片,可以定义结构体或使用已经存在的结构进行结构体数组声明。name属性用来描述特定的测试用例。

接下来让我们试着自己编写表格驱动测试:

func TestSplitAll(t *testing.T) {// 定义测试表格// 这里使用匿名结构体定义了若干个测试用例// 并且为每个测试用例设置了一个名称tests := []struct {name  stringinput stringsep   stringwant  []string}{{"base case", "a:b:c", ":", []string{"a", "b", "c"}},{"wrong sep", "a:b:c", ",", []string{"a:b:c"}},{"more sep", "abcd", "bc", []string{"a", "d"}},{"leading sep", "沙河有沙又有河", "沙", []string{"", "河有", "又有河"}},}// 遍历测试用例for _, tt := range tests {t.Run(tt.name, func(t *testing.T) { // 使用t.Run()执行子测试got := Split(tt.input, tt.sep)if !reflect.DeepEqual(got, tt.want) {t.Errorf("expected:%#v, got:%#v", tt.want, got)}})}
}

在终端执行go test -v,会得到如下测试输出结果:

❯ go test -v
=== RUN   TestSplit
--- PASS: TestSplit (0.00s)
=== RUN   TestSplitWithComplexSep
--- PASS: TestSplitWithComplexSep (0.00s)
=== RUN   TestSplitAll
=== RUN   TestSplitAll/base_case
=== RUN   TestSplitAll/wrong_sep
=== RUN   TestSplitAll/more_sep
=== RUN   TestSplitAll/leading_sep
--- PASS: TestSplitAll (0.00s)--- PASS: TestSplitAll/base_case (0.00s)--- PASS: TestSplitAll/wrong_sep (0.00s)--- PASS: TestSplitAll/more_sep (0.00s)--- PASS: TestSplitAll/leading_sep (0.00s)
PASS
ok      golang-unit-test-demo/base_demo 0.010s

并行测试

表格驱动测试中通常会定义比较多的测试case,在Go语言中很容易发挥自身并发优势将表格驱动测试并行化,可以查看下面的代码示例。

func TestSplitAll(t *testing.T) {t.Parallel()  // 将 TLog 标记为能够与其他测试并行运行// 定义测试表格// 这里使用匿名结构体定义了若干个测试用例// 并且为每个测试用例设置了一个名称tests := []struct {name  stringinput stringsep   stringwant  []string}{{"base case", "a:b:c", ":", []string{"a", "b", "c"}},{"wrong sep", "a:b:c", ",", []string{"a:b:c"}},{"more sep", "abcd", "bc", []string{"a", "d"}},{"leading sep", "沙河有沙又有河", "沙", []string{"", "河有", "又有河"}},}// 遍历测试用例for _, tt := range tests {tt := tt  // 注意这里重新声明tt变量(避免多个goroutine中使用了相同的变量)t.Run(tt.name, func(t *testing.T) { // 使用t.Run()执行子测试t.Parallel()  // 将每个测试用例标记为能够彼此并行运行got := Split(tt.input, tt.sep)if !reflect.DeepEqual(got, tt.want) {t.Errorf("expected:%#v, got:%#v", tt.want, got)}})}
}

使用工具生成测试代码

社区里有很多自动生成表格驱动测试函数的工具,比如gotests等,很多编辑器如Goland也支持快速生成测试文件。这里简单演示一下gotests的使用。

安装

go get -u github.com/cweill/gotests/...

执行

gotests -all -w split.go

上面的命令表示,为split.go文件的所有函数生成测试代码至split_test.go文件(目录下如果事先存在这个文件就不再生成)。

生成的测试代码大致如下:

package base_demoimport ("reflect""testing"
)func TestSplit(t *testing.T) {type args struct {s   stringsep string}tests := []struct {name       stringargs       argswantResult []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)}})}
}

代码格式与我们上面的类似,只需要在TODO位置添加我们的测试逻辑就可以了。

测试覆盖率

测试覆盖率是指代码被测试套件覆盖的百分比。通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。在公司内部一般会要求测试覆盖率达到80%左右。

Go提供内置功能来检查你的代码覆盖率。我们可以使用go test -cover来查看测试覆盖率。例如:

❯ go test -cover
PASS
coverage: 100.0% of statements
ok      golang-unit-test-demo/base_demo 0.009s

从上面的结果可以看到我们的测试用例覆盖了100%的代码。

Go还提供了一个额外的-coverprofile参数,用来将覆盖率相关的记录信息输出到一个文件。例如:

❯ go test -cover -coverprofile=c.out
PASS
coverage: 100.0% of statements
ok      golang-unit-test-demo/base_demo 0.009s

上面的命令会将覆盖率相关的信息输出到当前文件夹下面的c.out文件中。

❯ tree .
.
├── c.out
├── split.go
└── split_test.go

然后我们执行go tool cover -html=c.out,使用cover工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成一个HTML报告。上图中每个用绿色标记的语句块表示被覆盖了,而红色的表示没有被覆盖。

testify/assert

testify是一个社区非常流行的Go单元测试工具包,其中使用最多的功能就是它提供的断言工具——testify/asserttestify/require

安装

go get github.com/stretchr/testify

使用示例

我们在写单元测试的时候,通常需要使用断言来校验测试结果,但是由于Go语言中没有提供断言,所以我们会写出很多的if...else...语句。而testify/assert为我们提供了很多常用的断言函数,并且能够输出友好、易于阅读的错误描述信息。

比如我们之前在TestSplit测试函数中就使用了reflect.DeepEqual来判断期望结果与实际结果是否一致。

t.Run(tt.name, func(t *testing.T) { // 使用t.Run()执行子测试got := Split(tt.input, tt.sep)if !reflect.DeepEqual(got, tt.want) {t.Errorf("expected:%#v, got:%#v", tt.want, got)}
})

使用testify/assert之后就能将上述判断过程简化如下:

t.Run(tt.name, func(t *testing.T) { // 使用t.Run()执行子测试got := Split(tt.input, tt.sep)assert.Equal(t, got, tt.want)  // 使用assert提供的断言函数
})

当我们有多个断言语句时,还可以使用assert := assert.New(t)创建一个assert对象,它拥有前面所有的断言方法,只是不需要再传入Testing.T参数了。

func TestSomething(t *testing.T) {assert := assert.New(t)// assert equalityassert.Equal(123, 123, "they should be equal")// assert inequalityassert.NotEqual(123, 456, "they should not be equal")// assert for nil (good for errors)assert.Nil(object)// assert for not nil (good when you expect something)if assert.NotNil(object) {// now we know that object isn't nil, we are safe to make// further assertions without causing any errorsassert.Equal("Something", object.Value)}
}

testify/assert提供了非常多的断言函数,这里没办法一一列举出来,大家可以查看官方文档了解。

testify/require拥有testify/assert所有断言函数,它们的唯一区别就是——testify/require遇到失败的用例会立即终止本次测试。

此外,testify包还提供了mock、http等其他测试工具,篇幅所限这里就不详细介绍了,有兴趣的同学可以自己了解一下。

总结

本文介绍了Go语言单元测试的基本用法,通过为Split函数编写单元测试的真实案例,模拟了日常开发过程中的场景,一步一步详细介绍了表格驱动测试、回归测试和常用的断言工具testify/assert的使用。在下一篇中,我们将更进一步,详细介绍如何使用httptest和gock工具进行网络测试。

- END -

扫码关注公众号「网管叨bi叨」

给网管个星标,第一时间吸我的知识 

Go单元测试从入门到放弃—0.单元测试基础相关推荐

  1. shell逻辑运算符优先级_ios从入门到放弃之C基础巩固运算符

    继续接着上一次ios从入门到放弃之C基础巩固-----printf函数.scanf函数.scanf函数实现原理来巩固C语言基础,这次学习的是一个非常不起眼的语言基础----运算符,这里主要是对一些比较 ...

  2. 深度学习---从入门到放弃(一)pytorch基础

    深度学习-从入门到放弃(一)pytorch Tensor 类似于numpy的array,pandas的dataframe:在pytorch里的数据结构是tensor,即张量 tensor简单操作 1. ...

  3. Lwip从入门到放弃之(一)---基础网络知识扫盲

    Lwip从入门到放弃之-基础网络知识扫盲(一) 由于工作中用到了有关Lwip的有关知识,本人作为一个网络通信协议的门外汉,打算系统的学习一下以太网通讯的有关知识.而Lwip作为一款开源的轻量级TCP/ ...

  4. tensorflow从入门到放弃-0

    刚接触tensorflow一周,感觉还是有点难度的.遇到这么个问题 failed call to cuInit: CUDA_ERROR_NO_DEVICE: no CUDA-capable devic ...

  5. ADRC从入门到放弃0

    ADRC:自抗扰控制器. 其中,在对自抗扰理论的 研究中,引入了带宽概念这一个崭新的思路,极大的简化了自抗扰技术使用时参数整定 的问题,同时时域分析的方法可以发现利用带宽的概念还有利于解决系统误差:采 ...

  6. Python编程从入门到放弃 - Part 1基础知识习题解析

    目录 第1章 起步 第2章 变量和简单数据类型 第3章 列表简介 第4章 操作列表 第5章 if语句 第6章 字典 第7章 用户输入和while循环 第8章 函数 第9章 类 第10章 文件和异常 第 ...

  7. 深度学习---从入门到放弃(九)RNN入门

    深度学习-从入门到放弃(九)RNN入门 1.RNN简介 RNN(Recurrent Neural Network)是一类用于处理序列数据的神经网络.回想一下我们之前说到过的CNN,它可以通过在空间上共 ...

  8. k8s dashboard_【大强哥-k8s从入门到放弃02】Kubernetes1.17部署Dashboard2.0

    号外号外,后面所有提升视频都会更新到知乎和B站上去,不会直接发群里了,哈哈,能看懂这句话的我都认识,大家可以先关注一下,我知乎上的所有文档也会录成视频 更多视频详见 杨哥天云:https://spac ...

  9. board crt_【大强哥-k8s从入门到放弃02】Kubernetes1.17部署Dashboard2.0

    号外号外,后面所有提升视频都会更新到知乎和B站上去,不会直接发群里了,哈哈,能看懂这句话的我都认识,大家可以先关注一下,我知乎上的所有文档也会录成视频 我马上就开B站账号,录制点儿走心的视频,呼呼 开 ...

最新文章

  1. FlashFXP客户端 FTP连接,连接很慢的情况,
  2. VHDL | 音乐盒设计(代码类)
  3. Json,Hashlib与Base64(占坑待补)
  4. 在公网(internet)上建立website时不能用http访问
  5. python英文词云代码_使用python实现个性化词云的方法
  6. sql between包括两端吗_SQL简单查询
  7. 叶修手速900什么概念_什么是数学?数学是探索的过程
  8. eclipse中文乱码解决_Stata中文乱码顽疾解决方法-一行命令
  9. apache的开源项目-模板引擎(Velocity)(转)
  10. Python 地图篇 - 使用pyecharts绘制世界地图、中国地图、省级地图、市级地图实例详解
  11. Python优化算法06——人工鱼群算法
  12. Encrypt java_ESAPI对称加密使用JavaEncryptor
  13. 【工具】“Error while running C:\ti\xdctools_3_31_01_33_core/xs --xdcpath=C:\ti\tir……”
  14. 计算机英语词汇带音标,计算机专用英语词汇1500词(带音标).doc
  15. Go切片(slices)
  16. 自己写C#用汽车诊断卡对MIT电机做调试
  17. redis分布式锁-redisson
  18. openGL中Phong 着色
  19. COBOL中的基本语法(转)
  20. 通过U盘传播的“病毒”

热门文章

  1. 安卓程序员都懂:如何用Espresso对UI界面测试?
  2. Percona XtraBackup User Manual 阅读笔记
  3. 2.请求安全-- MD5的必要性以及实际应用场景
  4. [翻译]Django tutorial, part 1: Models
  5. LeetCode-数组-三数之和
  6. [CTSC2008]网络管理Network
  7. 树(5)-----判断两颗树一样或者一棵树是否是另外一颗的子树
  8. IO is frozen on database xxx, No user action is required
  9. 拯救不靠谱:他是怎样将技术外包做到纠纷率3%?
  10. HDU2012 素数判定