作者选择FreeBSD 基金会作为Write for DOnations计划的一部分接受捐赠。
如何在 Go Usinggo test
和testing
Package 中编写单元测试
介绍
单元测试是测试程序或包中特定代码段的功能。单元测试的工作是检查应用程序的正确性,它们是Go 编程语言的关键部分。
在本教程中,您将创建一个小程序,然后使用 Go 的testing
包和go test
命令对您的代码运行一系列测试。完成本教程后,您将拥有一个工作单元测试套件,其中包括基于表格的单元测试、覆盖测试、基准测试和文档化示例。
先决条件
要完成本教程,您需要具备以下条件:
-
熟悉 Go 编程语言。请访问我们的教程系列/电子书How To Code in Go,了解该语言的广泛介绍。
-
Go 1.11或更高版本安装在您的本地机器上。您可以按照这些说明在Linux、macOS和Windows上安装 Go 。在 macOS 上,您还可以使用 Homebrew 包管理器安装 Go。
注意:本教程使用 Go Modules,它是 Go 1.11 版本中引入的包管理系统。Go Modules 旨在取代 $GOPATH 并从 Go 1.13 版开始成为默认选项。要更全面地了解 Go Modules 和 $GOPATH 之间的差异,请考虑阅读 Go 核心团队的这篇官方博客文章。
本教程使用 Go 1.14 版进行测试
第 1 步 – 创建一个示例程序进行单元测试
在编写任何单元测试之前,您需要一些代码来分析测试。在这一步中,您将构建一个对两个整数求和的小程序。在随后的步骤中,您将使用go test
来测试该程序。
首先,创建一个名为 的新目录math
:
- mkdir ./math
在新目录中移动:
- cd ./math
这将是您的程序的根目录,您将从这里运行所有剩余的命令。
现在,使用nano
或您喜欢的文本编辑器,创建一个名为 的新文件math.go
:
- nano 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
:
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
ort.Fail
以指示失败(您正在调用t.Error
是因为它返回的详细信息比t.Fail
) - 您可以
t.Log
用来提供非失败的调试信息 - 测试使用以下命名约定保存在文件中:
foo_test.go
,例如math_test.go
.
保存然后关闭文件。
在这一步中,您用 Go 编写了第一个测试。在下一步中,您将开始使用go test
来测试您的代码。
第 3 步 – 使用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
删除文件中的所有代码并添加以下表驱动单元测试:
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
将打开一个网络浏览器,您的结果将呈现:
绿色文本表示覆盖范围,而红色文本表示相反。
在此步骤中,您测试了表驱动单元测试的覆盖率。在下一步中,您将对您的函数进行基准测试。
第 6 步 — 在 Go 中编写基准
在这一步中,您将使用 Go编写一个基准测试。基准测试衡量功能或程序的性能。这使您可以比较实现并了解对代码所做更改的影响。使用该信息,您可以揭示 Go 源代码中值得优化的部分。
在 Go 中,采用这种形式的函数func BenchmarkXxx(*testing.B)
被视为基准。go test
当您提供-bench
标志时,将执行这些基准测试。基准测试按顺序运行。
让我们为单元测试添加一个基准测试。
打开math_test.go
:
- nano math_test.go
现在使用以下func BenchmarkXxx(*testing.B)
语法添加 benchamrk 函数:
...
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
包到导入列表,并在文件末尾你的榜样作用:
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
您的示例现在也经过测试。此功能改进了您的文档,并使您的单元测试更加健壮。
结论
在本教程中,您创建了一个小程序,然后编写了一个基本的单元测试来检查其功能。然后,您将单元测试重写为基于表格的单元测试,然后添加了覆盖测试、基准测试和文档化示例。
作为程序员,花时间编写足够的单元测试对您很有用,因为它可以提高您对编写的代码或程序将继续按预期工作的信心。testing
Go 中的包为您提供了大量的单元测试功能。要了解更多信息,请参阅 Go 的官方文档。
如果您想了解有关 Go 编程的更多信息,请访问我们的教程系列/免费电子书 How To Code in Go。