Golang语言是现在炙手可热的编程语言之一。每个人可能都或多或少的学习和用Golang在写东西。在程序开发中,测试是一个中必不可少的部分。
有句话”工欲善其事,必先利其器”,测试就是开发中值得花功夫”磨快刀”的开发利器。本文我们就一起来学习下如何在Golang测试理念及测试工具。
概述
Golang中附带了用于编写和运行测试工具:标准库 testing 包以 go test 命令行工具运行测试套件。和Golang语言本身的设计理念一致,Golang测试理念很简单:用轻量级的测试包和Golang帮助函数结合实现。在这样的理念下测试也是代码。由于Golang开发人员已经知道如何使用抽象和类型编写Go,因此无需在额外学习特定的测试语法没个人可能都或多或少和配置。看下面一个简单的实例,用testing 测试Abs函数简单代码:
func Test Abs (t *testing.T) {
go t := Abs(-1)
if got != 1 {
t.Errorf("Abs(-1) = %d; want 1", got)
}
}
与上面的代码对比,下面是使用Ginkgo库编写的实例。Ginkg库为Golang提供了编写RSpec风格的测试方法:
Describe("Abs", func() {
It("returns correct abs value for -1", func() {
got := Abs(-1)
Expect(got).To(Equal(1))
})
})
两种表达方式,显然第一种方法对Golang码农来说,更加易于理解。 RSpec风格 中使用诸如函数 Describe,Expect 的语法模仿了人类语言的表达体验。但是但是对golang码农来说,这来的可能会很突兀,需要重新学习和适应。
另一个相对轻量级的库是testify/assert,它添加了诸如assert.Equal()之类的通用断言函数。
testify/suite则添加了诸如setup和teardown之类的测试套件实用工具。
“Awesome Go”网站提供了此类第三方 软件包 的详尽列表。
还一个不包含在测试包中的有用的测试工具是reflect.DeepEqual(),这是一个标准库函数,它用反射来测试深度相等性,即跟随指针并递归到映射,数组等之后的相等性。当测试比较JSON对象或带有指针的结构之类的东西时,该函数非常有用。以此为基础的两个库是谷歌的 go-cmp 软件包和Daniel Nichter的 deep ,它们类似于DeepEqual,但是格式规范为,更加适应人类阅读的方式。例如,下面使用go-cmp对MakeUsers()函数进行的测试:
func TestMakeUser(t *testing.T) {
got := MakeUser("Chongchong", "congcong@ijz.me", 28)
want := &User{
Name: "Chongchong",
Email: "cc@ijz.me",
Age: 28,
}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("MakeUser() mismatch (-want +got):\n%s", diff)
}
}
其输出更加人性化:
user_test.go:16: MakeUser() mismatch (-want +got):
&main.User{
Name: " Chongchong",
- Email: "cc@ijz.me",
+ Email: "congcong@ijz.me",
Age: 28,
}
内置测试功能
Golang内置的testing包携带了各种功能,可记录信息和报告故障,在运行时跳过测试或仅以简短模式运行测试。简短模式提供了一种跳过长时间运行或具有大量设置的测试的方法,这在开发过程中可能会有所帮助。可以用-test.short命令行参数启用它。
Go的测试运行程序默认情况下按顺序执行测试,但提供一个可选的Parallel()函数允许跨多个内核同时运行带有显著标记的测试。
在Go 1.14中,测试包添加了Cleanup()函数,该函数注册了一个在测试完成时要调用的函数。
这是一种简化拆卸的内置方法,例如,在测试完成后删除数据库表:
func createDatabase(t *testing.T) {
// ...创建测试数据库的代码
t.Cleanup(func() {
// ... 当测试结束时,清除数据库的代码code to delete the test
})
}
func TestFetchUser(t *testing.T) {
createDatabase(t) // 创建数据,注册cleanup
user, err := FetchUser("cc@ijz.me")
if err != nil {
t.Fatalf("error fetching user: %v", err)
}
expected := &User{"Chongchong", " cc@ijz.me", 28}
if !reflect.DeepEqual(user, expected) {
t.Fatalf("expected user %v, got %v", expected, user)
}
}
Go 1.15添加了一个测试助手TempDir(),该助手为当前测试创建(并清理)了一个临时目录。
表驱动测试
Golang中常见的习惯用法是在测试各种边缘情况时避免重复,称为”表驱动测试”。该技术迭代”切片”中的 测试用例 ,报告每次迭代的失败:
func TestAbs(t *testing.T) {
tests := [] struct {
input int
expected int
}{
{1, 1},
{0, 0},
{-1, 1},
{-maxInt, maxInt},
{maxInt, maxInt},
}
for _, test := range tests {
actual := Abs(test.input)
if actual != test.expected {
t.Errorf("Abs(%d) = %d; want %d", test.input, actual, test.expected)
}
}
}
该t.Errorf()的调用提示存在的问题,但不会停止测试的执行,所以可以报告多个问题。在标准库测试(例如fmt测试)中,这种表驱动测试的样式很常见。Golang 1.7引入了一个功能Subtests,它让运行在命令行中单独进行部分子测试,可以更好的控制在失败和平行执行。
模拟和接口
Go的著名语言功能之一是其结构类型化的接口,也被称为”编译时鸭子类型 =”。每当需要在运行时改变行为时,接口就很重要,当然其中包括测试。
例如,正如Golang核心开发者Andrew Gerrand在2014年”测试技术”演讲的所举过的例子:文件格式解析器不应像这样传递具体的文件类型:
func Parse(f *os.File) error { ... }
而,Parse()应该仅采用一个仅实现所需功能的小型接口。在这种情况下,无处不在的io. Reader 是一个不错的选择:
func Parse(r io.Reader) error { ... }
这样,就可以向解析器提供任何能实现io.Reader的东西,包括文件,字符串缓冲区和网络连接等。它还使测试变得更加容易(可能使用strings.Reader)。
如果测试仅使用大型接口的一小部分,例如来自多个方法API的一种方法,则可以创建一种新的结构类型,该结构类型嵌入接口以实现API合同,并且仅覆盖被调用的方法。。
有各种第三方工具,例如GoMock和mockery,可以从接口定义自动生成模仿代码。
测试用例
Go的软件包文档是从源代码中的注释生成的。与Javadoc或C#的文档系统在代码注释中大量使用标记不同,Golang的方法是,源代码中的注释仍应在源代码中可读,而不不是满篇幅的标记。
它采用与文档示例类似的方法:下面是可运行的代码片段,这些片段在运行测试时自动执行,然后包含在生成的文档中。就像Python的doctests一样,测试用例将写入标准输出,并将输出和期待的输出进行比较,避免文档示例中的回归。这是一个Abs()函数的测试用例:
func ExampleAbs() {
fmt.Println(Abs(5))
fmt.Println(Abs(-42))
// Output:
// 5
// 42
}
示例函数必须位于*_test.go文件中,并添加Example前缀。当测试运行程序执行时,会自动解析Output:注释并将其与实际输出进行比较,如果它们不同,则测试失败。这些示例作为可运行的Go Playground代码段包含在生成的文档中,
基准测试
除测试外,该测试包还允许运行定时基准测试。这些在整个标准库中大量使用,以确保执行速度不下降。可以使用带有-bench =选项的go test自动运行基准测试。
例如,下面标准库的strings.TrimSpace()函数的基准测试用例:
func BenchmarkTrimSpace(b *testing.B) {
tests := []struct{ name, input string }{
{"NoTrim", "typical"},
{"ASCII", " foo bar "},
{"SomeNonASCII", " \u2000\t\r\n x\t\t\r\r\ny\n \u3000 "},
{"JustNonASCII", "\u2000\u2000\u2000☺☺☺☺\u3000\u3000\u3000"},
}
for _, test := range tests {
b.Run(test.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
TrimSpace(test.input)
}
})
}
在测试工具将报表中的数字;诸如Benchstat之类的程序可用于比较之前和之后的时间。Benchstat的输出通常包含在Go的提交消息中,这些消息显示了性能的提高。例如,一个go性能报告中
报告显示,”SomeNonASCII” 测试的速度降低了约9%,但TrimSpace的ASCII快速路径使纯ASCII输入的速度提高了约5倍。
要诊断某些地方运行缓慢,可以使用内置的profiling性能分析工具,例如运行测试时的-cpuprofile选项。内置的工具pprof以多种格式显示性能结果,包括火焰图。
go test命令
社区中对Golang对测试目录测试用例(在名为*_test.go的文件中)以及测试函数的命名方式(必须有Test前缀)很有意见。但是,go test工具确实一个很好用的工具,它可以确切地知道去哪里查找并运行测试用例。不需要额外的用于描述测试存放位置makefile或元数据。如果文件和函数是以标准方式命名的,其他的go test就可以自动搞定。go test命令表面很简单,但是其选项不少也很繁琐:
go test :在当前目录中运行测试
go test package :对给定的包运行测试
go test ./… :对当前目录和所有子包运行测试
go test -run=foo :运行与正则表达式”foo”匹配的测试
go test -cover :运行测试并输出代码覆盖率
go test -bench=. :#同时运行基准
go test -bench=. -cpuprofile cpu.out :运行基准测试,记录性能分析信息
go test的 -cover模式会生成代码覆盖率配置文件,可以使用 go tool cover -html = coverage.out 将转为html格式。
总结
Go的测试库简单方便,而且可扩展,go test命令行工具进行测试执行,并进行结果处理、基准测试、性能分析和代码覆盖率等分析。go testing包中干货满满,需要我们长时间去探索。当然golang也不排除第三测试模块,获取更多的测试体验,当然你也可以开发适合自己的测试模块,所有这些在Golang都是可能。