如何在Go中编写单元测试
作为 Write for DOnations 计划的一部分,作者选择了 FreeBSD Foundation 来接受捐赠。
介绍
单元测试是测试程序或包中特定代码段的功能。 单元测试的工作是检查应用程序的正确性,它们是 Go 编程语言 的关键部分。
在本教程中,您将创建一个小程序,然后使用 Go 的测试包和 go test 命令 对您的代码 运行一系列测试。 完成本教程后,您将拥有一个工作单元测试套件,其中包括基于 表的单元测试 、 覆盖测试 、 基准测试 和一个记录的例子。
先决条件
要完成本教程,您需要以下内容:
- 熟悉 Go 编程语言。 访问我们的教程系列/电子书,如何在 Go 中编码,了解该语言的广泛介绍。
- Go 版本 1.11 或更高版本安装在本地计算机上。 您可以按照这些说明在 Linux、macOS 和 Windows 上安装 Go。 在 macOS 上,您还可以 使用 Homebrew 包管理器 安装 Go。
注:本教程使用Go Modules,这是Go 1.11版本引入的包管理系统。 Go Modules 旨在取代 $GOPATH 并成为从 Go 版本 1.13 开始的默认选项。 要更全面地了解 Go 模块和 $GOPATH 之间的差异, 考虑阅读 Go 核心团队 的这篇官方博客文章。
本教程使用 Go 版本 1.14 进行了测试
第 1 步 - 创建示例程序以进行单元测试
在您编写任何单元测试之前,您需要一些代码来分析您的测试。 在这一步中,您将构建一个对两个整数求和的小程序。 在后续步骤中,您将使用 go test
来测试程序。
首先,创建一个名为 math
的新目录:
mkdir ./math
进入新目录:
cd ./math
这将是您的程序的根目录,您将从这里运行所有剩余的命令。
现在,使用 nano
或您喜欢的文本编辑器,创建一个名为 math.go
的新文件:
nano math.go
添加以下代码:
./math/math.go
package math // Add is our function that sums two integers func Add(x, y int) (res int) { return x + y } // Subtract subtracts two integers func Subtract(x, y int) (res int) { return x - y }
在这里,您将创建两个函数,称为 Add
和 Subtract
。 每个函数接受两个整数并返回它们的和 (func Add
) 或它们的差 (func Subtract
)。
保存并关闭文件。
在这一步中,您使用 Go 编写了一些代码。 现在,在以下步骤中,您将编写一些单元测试以确保您的代码正常运行。
第 2 步 — 在 Go 中编写单元测试
在这一步中,您将使用 Go 编写您的第一个测试。 在 Go 中编写测试需要一个测试文件链接,并且该测试文件必须始终以 _test.go
结尾。 按照惯例,Go 测试文件总是位于它们正在测试的代码所在的同一个文件夹或包中。 当您运行 go build
命令时,编译器不会生成这些文件,因此您不必担心它们最终会在部署中结束。
与 Go 中的所有内容一样,该语言对测试持固执己见。 Go 语言提供了一个最小但完整的包,称为 testing,开发人员可以将其与 go test
命令一起使用。 testing
包提供了一些有用的约定,例如覆盖测试和基准测试,您现在将探索这些约定。
使用编辑器创建并打开一个名为 math_test.go
的新文件:
nano math_test.go
Go 中的一个测试函数包括这个签名:func TestXxxx(t *testing.T)
。 这意味着所有测试函数必须以单词 Test
开头,后跟一个首字母大写的后缀。 Go 中的测试函数只接收一个参数,在这种情况下,它是一个 testing.T
类型的指针。 此类型包含有用的方法,您需要输出结果、将错误记录到屏幕和信号故障,例如 t.Errorf()
方法。
将以下代码添加到 math_test.go
:
./math/math_test.go
package math import "testing" func TestAdd(t *testing.T){ got := Add(4, 6) want := 10 if got != want { t.Errorf("got %q, wanted %q", got, want) } }
首先,声明要测试的包的名称——math
。 然后导入 testing
包本身,然后使 testing.T
类型以及包导出的其他类型和方法可用。 代码和测试逻辑包含在 TestAdd
函数中。
总而言之,以下是 Go 中测试的特征:
- 第一个也是唯一的参数必须是
t *testing.T
- 测试函数以单词
Test
开头,后跟以大写字母开头的单词或短语(惯例是使用被测方法的名称,例如TestAdd
) - 测试调用
t.Error
或t.Fail
以指示失败(您正在调用t.Error
,因为它返回的细节比t.Fail
更多) - 您可以使用
t.Log
提供非失败调试信息 - 测试使用以下命名约定保存在文件中:
foo_test.go
,例如math_test.go
。
保存然后关闭文件。
在这一步中,您使用 Go 编写了第一个测试。 在下一步中,您将开始使用 go test
来测试您的代码。
第三步——使用 go test
命令测试你的 Go 代码
在此步骤中,您将测试您的代码。 go test
是一个强大的子命令,可帮助您自动化测试。 go test
接受不同的标志,这些标志可以配置您希望运行的测试、测试返回的详细程度等等。
在项目的根目录中,运行您的第一个测试:
go test
您将收到以下输出:
OutputPASS ok ./math 0.988s
PASS
表示代码按预期工作。 当测试失败时,您将看到 FAIL
。
go test
子命令只查找带有 _test.go
后缀的文件。 go test
然后扫描这些文件以查找特殊功能,包括 func TestXxx
和我们将在后面的步骤中介绍的其他几个功能。 go test
然后生成一个临时主包,它以正确的方式调用这些函数,构建并运行它们,报告结果,最后清理所有内容。
对于我们的小程序,我们的 go test
可能已经足够了,但是有时您会希望查看正在运行哪些测试以及每个测试需要多长时间。 添加 -v
标志会增加详细程度。 使用新标志重新运行您的测试:
go test -v
您将看到以下输出:
Output=== RUN TestAdd --- PASS: TestAdd (0.00s) PASS ok ./math 1.410s
在此步骤中,您使用 go test
子命令运行了基本单元测试。 在下一步中,您将编写一个更复杂的、表驱动的单元测试。
第 4 步 — 在 Go 中编写表驱动测试
表驱动测试类似于基本单元测试,只是它维护一个包含不同值和结果的表。 测试套件迭代这些值并将它们提交给测试代码。 使用这种方法,我们可以测试几种输入组合及其各自的输出。
现在,您将用结构表替换单元测试,其字段包括 Add
函数所需的两个参数(两个整数)和预期结果(它们的总和)。
重新打开 math_test.go
:
nano math_test.go
删除文件中的所有代码并添加以下表驱动单元测试:
./math/math_test.go
package math import "testing" // arg1 means argument 1 and arg2 means argument 2, and the expected stands for the 'result we expect' type addTest struct { arg1, arg2, expected int } var addTests = []addTest{ addTest{2, 3, 5}, addTest{4, 8, 12}, addTest{6, 9, 15}, addTest{3, 10, 13}, } func TestAdd(t *testing.T){ for _, test := range addTests{ if output := Add(test.arg1, test.arg2); output != test.expected { t.Errorf("Output %q not equal to expected %q", output, test.expected) } } }
在这里,您要定义一个结构,填充一个结构表,其中包含 Add
函数的参数和预期结果,然后编写一个新的 TestAdd
函数。 在这个新函数中,您遍历表,运行参数,将输出与每个预期结果进行比较,然后在发生错误时返回任何错误。
保存并关闭文件。
现在使用 -v
标志运行测试:
go test -v
您将看到与以前相同的输出:
Output=== RUN TestAdd --- PASS: TestAdd (0.00s) PASS ok ./math 1.712s
对于循环的每次迭代,代码都会根据预期值测试 Add
函数计算的值。
在这一步中,您编写了一个表驱动测试。 在下一步中,您将改为编写覆盖率测试。
第 5 步 — 在 Go 中编写覆盖测试
在这一步中,您将在 Go 中编写 覆盖测试。 在编写测试时,了解测试覆盖了多少实际代码通常很重要。 这通常称为覆盖。 这也是您没有为 Subtract
函数编写测试的原因——因此我们可以查看不完整的覆盖测试。
运行以下命令来计算当前单元测试的覆盖率:
go test -coverprofile=coverage.out
您将收到以下输出:
OutputPASS coverage: 50.0% of statements ok ./math 2.073s
Go 将此覆盖率数据保存在文件 coverage.out
中。 现在您可以在 Web 浏览器中显示结果。
运行以下命令:
go tool cover -html=coverage.out
将打开一个 Web 浏览器,您的结果将呈现:
绿色文字表示覆盖范围,而红色文字表示相反。
在这一步中,您测试了表驱动单元测试的覆盖率。 在下一步中,您将对函数进行基准测试。
第 6 步 — 在 Go 中编写基准
在这一步中,您将在 Go 中编写 基准测试。 基准测试衡量功能或程序的性能。 这使您可以比较实现并了解您对代码所做的更改的影响。 使用这些信息,您可以揭示 Go 源代码中值得优化的部分。
在 Go 中,采用 func BenchmarkXxx(*testing.B)
形式的函数被视为基准。 当您提供 -bench
标志时,go test
将执行这些基准测试。 基准测试按顺序运行。
让我们在单元测试中添加一个基准。
打开math_test.go
:
nano math_test.go
现在使用 func BenchmarkXxx(*testing.B)
语法添加一个 benchamrk 函数:
./math_test.go
... func BenchmarkAdd(b *testing.B){ for i :=0; i < b.N ; i++{ Add(4, 6) } }
基准函数必须运行目标代码 bN 次,其中 N 是可以调整的整数。 在基准执行期间,调整 bN 直到基准函数持续足够长的时间以可靠地计时。 --bench
标志接受正则表达式形式的参数。
保存并关闭文件。
现在让我们再次使用 go test
来运行我们的基准测试:
go test -bench=.
.
将匹配文件中的每个基准函数。
您还可以显式声明基准函数:
go test -bench=Add
运行任一命令,您将看到如下输出:
Outputgoos: windows goarch: amd64 pkg: math BenchmarkAdd-4 1000000000 1.07 ns/op PASS ok ./math 2.074s
结果输出意味着循环以每个循环 1.07 纳秒的速度运行了 10,000,000 次。
注意: 尽量不要在用于其他目的的繁忙系统上对您的 Go 代码进行基准测试,否则您会干扰基准测试过程并获得不准确的结果
您现在已经为不断增长的单元测试添加了一个基准。 在下一步也是最后一步中,您将向文档中添加示例,go test
也将评估这些示例。
第 7 步 — 用示例记录你的 Go 代码
在这一步中,您将 用示例记录您的 Go 代码,然后测试这些示例 。 Go 非常关注正确的文档,示例代码为文档和测试增加了另一个维度。 示例基于现有的方法和功能。 您的示例应该向用户展示如何使用特定的代码。 示例函数是由 go test
子命令特别处理的第三种类型的函数。
首先,重新打开 math_test.go
,
nano math_test.go
现在添加突出显示的代码。 这会将 fmt 包 添加到导入列表中,并将您的示例函数添加到文件末尾:
./math/math_test.go
package math import ( "fmt" "testing" ) // arg1 means argument 1 and arg2 means argument 2, and the expected stands for the 'result we expect' type addTest struct { arg1, arg2, expected int } var addTests = []addTest{ addTest{2, 3, 5}, addTest{4, 8, 12}, addTest{6, 9, 15}, addTest{3, 10, 13}, } func TestAdd(t *testing.T) { for _, test := range addTests { if output := Add(test.arg1, test.arg2); output != test.expected { t.Errorf("Output %q not equal to expected %q", output, test.expected) } } } func BenchmarkAdd(b *testing.B) { for i := 0; i < b.N; i++ { Add(4, 6) } } func ExampleAdd() { fmt.Println(Add(4, 6)) // Output: 10 }
Output:
行用于指定和记录预期的输出。
注意:比较忽略前导和尾随空格。
保存并关闭文件。
现在重新运行您的单元测试:
go test -v
您将看到如下更新的输出:
Output=== RUN TestAdd --- PASS: TestAdd (0.00s) === RUN ExampleAdd --- PASS: ExampleAdd (0.00s) PASS ok ./math 0.442s
您的示例现在也经过测试。 此功能改进了您的文档,也使您的单元测试更加健壮。
结论
在本教程中,您创建了一个小程序,然后编写了一个基本单元测试来检查其功能。 然后,您将单元测试重写为基于表的单元测试,然后添加了覆盖测试、基准测试和文档化示例。
作为程序员,花时间编写足够的单元测试对您很有用,因为它可以提高您对您编写的代码或程序将继续按预期工作的信心。 Go 中的 testing
包为您提供了相当多的单元测试功能。 要了解更多信息,请参阅 Go 的官方文档。
如果您想了解更多关于 Go 编程的信息,请访问我们的教程系列/免费电子书,如何在 Go 中编程。