❯ 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 TestSomething(t *testing.T) {
assert := assert.New(t)
// assert equality
assert.Equal(123, 123, "they should be equal")
// assert inequality
assert.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 errors
assert.Equal("Something", object.Value)
}
}
这是Go语言单元测试教程的第1篇,主要讲解在Go语言中如何做单元测试以及介绍了表格驱动测试、回归测试,并且介绍了常用的断言工具。
Go语言测试
go test工具
Go语言中的测试依赖
go test
命令。编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具。go test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以
_test.go
为后缀名的源代码文件都是go test
测试的一部分,不会被go build
编译到最终的可执行文件中。在
*_test.go
文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。go test
命令会遍历所有的*_test.go
文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。单元测试函数
格式
每个测试函数必须导入
testing
包,测试函数的基本格式(签名)如下:测试函数的名字必须以
Test
开头,可选的后缀名必须以大写字母开头,举几个例子:其中参数
t
用于报告测试失败和附加的日志信息。testing.T
的拥有的方法如下:单元测试示例
就像细胞是构成我们身体的基本单位,一个软件程序也是由很多单元组件构成的。单元组件可以是函数、结构体、方法和最终用户可能依赖的任意东西。总之我们需要确保这些组件是能够正常运行的。单元测试是一些利用各种方法测试单元组件的程序,它会将结果与预期输出进行比较。
接下来,我们在
base_demo
包中定义了一个Split
函数,具体实现如下:在当前目录下,我们创建一个
split_test.go
的测试文件,并定义一个测试函数如下:此时
split
这个包中的文件如下:在当前路径下执行
go test
命令,可以看到输出结果如下:go test -v
一个测试用例有点单薄,我们再编写一个测试使用多个字符切割字符串的例子,在
split_test.go
中添加如下测试函数:现在我们有多个测试用例了,为了能更好的在输出结果中看到每个测试用例的执行情况,我们可以为
go test
命令添加-v
参数,让它输出完整的测试结果。从上面的输出结果我们能清楚的看到是
TestSplitWithComplexSep
这个测试用例没有测试通过。go test -run
单元测试的结果表明
split
函数的实现并不可靠,没有考虑到传入的sep参数是多个字符的情况,下面我们来修复下这个Bug:在执行
go test
命令的时候可以添加-run
参数,它对应一个正则表达式,只有函数名匹配上的测试函数才会被go test
命令执行。例如通过给
go test
添加-run=Sep
参数来告诉它本次测试只运行TestSplitWithComplexSep
这个测试用例:最终的测试结果表情我们成功修复了之前的Bug。
回归测试
我们修改了代码之后仅仅执行那些失败的测试用例或新引入的测试用例是错误且危险的,正确的做法应该是完整运行所有的测试用例,保证不会因为修改代码而引入新的问题。
测试结果表明我们的单元测试全部通过。
通过这个示例我们可以看到,有了单元测试就能够在代码改动后快速进行回归测试,极大地提高开发效率并保证代码的质量。
跳过某些测试用例
为了节省时间支持在单元测试时跳过某些耗时的测试用例。
当执行
go test -short
时就不会执行上面的TestTimeConsuming
测试用例。子测试
在上面的示例中我们为每一个测试数据编写了一个测试函数,而通常单元测试中需要多组测试数据保证测试的效果。Go1.7+ 中新增了子测试,支持在测试函数中使用
t.Run
执行一组测试用例,这样就不需要为不同的测试数据定义多个测试函数了。表格驱动测试
介绍
编写好的测试并非易事,但在许多情况下,表格驱动测试可以涵盖很多方面:表格里的每一个条目都是一个完整的测试用例,包含输入和预期结果,有时还包含测试名称等附加信息,以使测试输出易于阅读。
使用表格驱动测试能够很方便的维护多个测试用例,避免在编写单元测试时频繁的复制粘贴。
表格驱动测试的步骤通常是定义一个测试用例表格,然后遍历表格,并使用
t.Run
对每个条目执行必要的测试。表格驱动测试不是工具、包或其他任何东西,它只是编写更清晰测试的一种方式和视角。
示例
官方标准库中有很多表格驱动测试的示例,例如fmt包中的测试代码:
通常表格是匿名结构体数组切片,可以定义结构体或使用已经存在的结构进行结构体数组声明。name属性用来描述特定的测试用例。
接下来让我们试着自己编写表格驱动测试:
在终端执行
go test -v
,会得到如下测试输出结果:并行测试
表格驱动测试中通常会定义比较多的测试case,在Go语言中很容易发挥自身并发优势将表格驱动测试并行化,可以查看下面的代码示例。
使用工具生成测试代码
社区里有很多自动生成表格驱动测试函数的工具,比如gotests等,很多编辑器如Goland也支持快速生成测试文件。这里简单演示一下
gotests
的使用。安装
执行
上面的命令表示,为
split.go
文件的所有函数生成测试代码至split_test.go
文件(目录下如果事先存在这个文件就不再生成)。生成的测试代码大致如下:
代码格式与我们上面的类似,只需要在TODO位置添加我们的测试逻辑就可以了。
测试覆盖率
测试覆盖率是指代码被测试套件覆盖的百分比。通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。在公司内部一般会要求测试覆盖率达到80%左右。
Go提供内置功能来检查你的代码覆盖率。我们可以使用
go test -cover
来查看测试覆盖率。例如:从上面的结果可以看到我们的测试用例覆盖了100%的代码。
Go还提供了一个额外的
-coverprofile
参数,用来将覆盖率相关的记录信息输出到一个文件。例如:上面的命令会将覆盖率相关的信息输出到当前文件夹下面的
c.out
文件中。然后我们执行
go tool cover -html=c.out
,使用cover
工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成一个HTML报告。上图中每个用绿色标记的语句块表示被覆盖了,而红色的表示没有被覆盖。testify/assert
testify是一个社区非常流行的Go单元测试工具包,其中使用最多的功能就是它提供的断言工具——
testify/assert
或testify/require
。安装
使用示例
我们在写单元测试的时候,通常需要使用断言来校验测试结果,但是由于Go语言中没有提供断言,所以我们会写出很多的
if...else...
语句。而testify/assert
为我们提供了很多常用的断言函数,并且能够输出友好、易于阅读的错误描述信息。比如我们之前在
TestSplit
测试函数中就使用了reflect.DeepEqual
来判断期望结果与实际结果是否一致。使用
testify/assert
之后就能将上述判断过程简化如下:当我们有多个断言语句时,还可以使用
assert := assert.New(t)
创建一个assert对象,它拥有前面所有的断言方法,只是不需要再传入Testing.T
参数了。testify/assert
提供了非常多的断言函数,这里没办法一一列举出来,大家可以查看官方文档了解。testify/require
拥有testify/assert
所有断言函数,它们的唯一区别就是——testify/require
遇到失败的用例会立即终止本次测试。此外,
testify
包还提供了mock、http等其他测试工具,篇幅所限这里就不详细介绍了,有兴趣的同学可以自己了解一下。总结
本文介绍了Go语言单元测试的基本用法,通过为Split函数编写单元测试的真实案例,模拟了日常开发过程中的场景,一步一步详细介绍了表格驱动测试、回归测试和常用的断言工具testify/assert的使用。在下一篇中,我们将更进一步,详细介绍如何使用httptest和gock工具进行网络测试。