模糊测试的用途
如前所述,编写涵盖所有可能的用户场景和参数值范围的测试非常困难。编写和维护的测试用例数量可能比项目工作更加耗时。在本节中,我们将探讨 Go 的模糊测试功能,它可以帮助我们编写涵盖各种输入的测试。
模糊测试(Fuzz Testing) 是一种强大的技术,已用于发现各种软件系统中的错误,包括 Go 标准库本身。它涉及生成各种值并将它们用作被测单元(UUT)的输入。生成的值对 UUT 进行压力测试,并帮助发现错误或意外行为,例如 panic
、内存泄漏或不正确的输出。
模糊测试是自动化的黑盒测试,可用于检测我们系统中的任何潜在功能或安全问题。它们通常使用模糊工具运行,该工具负责值生成、测试执行和错误检测。在本节中,我们将重点介绍使用模糊测试来检测功能错误。
图10.2 展示了模糊测试涉及的步骤概述:

编写模糊测试与常规单元测试没有太大区别:
-
识别模糊目标:就像普通测试一样,我们首先识别被测单元(UUT)。模糊目标将是我们将在测试中涵盖的函数。
-
识别模糊参数:模糊目标只有在至少接受一个参数时才适合进行模糊测试。这些参数将由模糊工具生成并用作先前识别的模糊目标的输入。
-
生成模糊值:一旦指定了测试,模糊工具将开始为我们的模糊参数生成随机值。
-
使用模糊值运行测试:使用生成的模糊值执行测试。通常,模糊测试是快速运行的单元测试,因为它们将使用大量生成的模糊值运行。
-
报告和记录失败:测试运行器将执行测试,记录并报告失败。就像单元测试一样,模糊测试可以包括断言和验证。
模糊测试可用于验证 UUT 或模糊目标未生成的任何输入值。这些值可以来自系统中的其他服务或外部来源。它可以应用于文件、策略、应用程序和库。
Go中的模糊测试
模糊测试的能力是一个全面测试策略的重要组成部分。模糊测试在 Go 1.18 版本中被添加到标准 Go 测试库中。这一功能备受 Go 社区的期待,使得编写模糊测试变得像编写单元测试一样容易。
就像测试和基准测试一样,模糊测试必须遵循一些规则:
-
测试必须以
Fuzz
前缀开头。我们注意到测试是导出的函数,以大写字母开头定义。例如,我们的GetSortedValues
函数的模糊测试可以命名为FuzzGetSortedValues
。 -
测试必须在以
_test.go
后缀命名的测试文件中定义。与其他测试文件一样,我们应该使用源文件的名称来命名我们的测试文件。例如,如果我们的排序函数定义在sort.go
文件中,那么其对应的测试文件可以是sort_test.go
。 -
测试必须接受一个
*testing.F
参数并且没有返回值。这是模糊测试与测试运行器和模糊工具交互的方式。 -
模糊目标通过在
*testing.F
参数上调用Fuzz
函数来定义。此函数接受一个*testing.T
参数,后跟模糊参数。每个测试只能有一个模糊目标,并且对 UUT 的调用将发生在模糊目标内部。 -
模糊参数使用
*testing.F
参数上的Add
函数添加到模糊工具中。这将指示工具生成用于模糊目标的值。 -
模糊参数可以是以下类型:
-
string
,[]byte
-
所有
int
类型,包括rune
-
所有
uint
类型,包括byte
-
所有
float
类型 -
bool
-
-
由于测试运行次数较多,模糊测试将并行运行。因此,它们应该是确定性的。
-
模糊测试与您的其他单元测试一起使用
go test
命令或带有-fuzz
标志后跟测试名称或包来运行。同样,这与我们运行基准测试的方式类似。
模糊工具监控测试运行并报告发生的错误。模糊测试可能因以下几个原因而失败:panic、测试失败、不可恢复的错误或超时。默认情况下,模糊目标的超时时间为 1 秒,因此您的测试应该很快。
模糊测试将继续运行,直到找到失败的输入或用户手动取消测试运行为止。或者,我们可以使用 –fuzztime
命令行参数提供最大执行时间或最大迭代次数。
一个简单模糊测试
我们可以为我们在上一节中编写的 GetSortedValues
函数编写一个简单的模糊测试:
func FuzzGetSortedValues_ASC(f *testing.F) {
input := map[int]string{
99: "B",
0: "A",
}
f.Add(3, "W")
f.Fuzz(func(t *testing.T, k int, v string) {
input[k] = v
keys := make([]int, 0, len(input))
for k := range input {
keys = append(keys, k)
}
sort.Ints(keys)
sortedValues, err := GetSortedValues(input, ASC)
require.Nil(t, err)
require.NotNil(t, sortedValues)
for index, v := range sortedValues {
key := keys[index]
assert.Equal(t, input[key], v)
}
})
}
我们根据我们讨论的规则并使用与单元测试相同的技术编写了模糊测试:
-
我们使用所需的签名声明了一个模糊测试。测试以
Fuzz
前缀开头并接受*testing.F
参数。 -
我们使用
f.Add
方法向模糊测试工具添加了两个模糊测试参数,一个用于int
类型的映射键,一个用于string
类型的映射值。这些值将由模糊工具生成。这两种类型都被接受为模糊参数。 -
我们使用
f.Fuzz
方法定义了模糊目标。此方法接收一个函数作为参数,该函数本身接收模糊参数作为参数。该函数还接收一个*testing.T
参数,这使得我们可以在模糊目标内部编写测试断言。 -
在模糊目标内部,我们编写了测试代码。我们将模糊参数添加到映射中,使用生成的值来测试我们的功能。然后,我们从映射中提取键并按升序排序。
-
我们调用了
GetSortedValues
函数,这是我们测试的 UUT,传递给它现在包含模糊参数的input
映射。 -
在测试结束时,我们使用之前排序的键切片来断言返回的值是否正确排序。
我们成功编写了第一个模糊测试。我们可以使用 go test
命令和两个配置标志来运行它:
$ go test -fuzz FuzzGetSortedValues_ASC -fuzztime 5s ./chapter10/fragile-revised -v
=== FUZZ FuzzGetSortedValues_ASC
fuzz: elapsed: 0s, gathering baseline coverage: 0/711 completed
fuzz: elapsed: 1s, gathering baseline coverage: 711/711 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 10707 (3569/sec), new interesting: 54 (total: 765)
fuzz: elapsed: 5s, execs: 17519 (3262/sec), new interesting: 67 (total: 778)
--- PASS: FuzzGetSortedValues_ASC (5.14s)
PASS
ok github.com/PacktPublishing/Test-Driven-Development-in-Go/chapter10/fragile-revised 5.303s
–fuzz
标志指示测试运行器执行按名称指定的模糊测试,而 –fuzztime
标志指定测试最多运行 5 秒。我们的测试运行的输出突出显示了测试运行的一些关键指标:
-
elapsed 表示处理时间
-
baseline coverage 表示用于测量测试提供的覆盖范围的场景数量
-
execs 表示已使用模糊目标运行的测试用例数量
-
new interesting 是识别出的扩展模糊测试覆盖范围的新输入数量
现在我们已经了解了如何编写和运行模糊测试,我们已准备好将它们添加到我们自己的测试策略中。然而,它确实有一些缺点。图10.3 展示了模糊测试的一些优点和缺点:

模糊测试的优点如下:
-
易于使用和实现:由于模糊测试与 Go 的测试包集成,我们可以轻松地为 Go 中的任何内容编写模糊测试。然而,保持测试范围较小很重要,以便它们可以快速有效地执行。
-
可以在开发生命周期的早期使用:正如我们在简单示例中看到的,可以为函数甚至小代码单元编写模糊测试。这使得在开发生命周期的任何阶段都可以轻松利用它们。
-
检测各种错误:模糊测试生成的值涵盖边缘情况并运行多次执行。这使其成为检测错误的绝佳工具,否则这些错误是不可能被发现的。
模糊测试的缺点如下:
-
不能替代传统测试:模糊测试是对我们在本书中探讨的测试类型的补充,而不是替代。因此,编写这些测试可能需要额外的工程工作。
-
不提供保证:模糊测试仅提供 UUT 稳定性的指示,而不是保证。由于它生成随机值,因此它只能向开发人员指示它确实涵盖的输入的错误存在。
-
内存和 CPU 密集型:正如我们从示例输出中看到的,模糊测试在大量场景中并行运行。这使得它们比单元测试更占用内存和 CPU。
尽管有这些缺点,模糊测试是一种有用且强大的测试技术,它补充了我们迄今为止探讨的所有传统测试方法。由于它能够生成各种输入,模糊测试也是帮助发现安全漏洞的重要工具。在安全模糊测试中,我们向程序输入恶意数据,而在功能模糊测试中,我们输入无效数据。我们不会在本书中重点介绍安全测试,但这是模糊测试的另一个重要用途。它对于确保我们的系统在边缘情况下或处理用户输入时保持稳定特别有用。