我们曾探讨过[如何有效地测试 Go 代码] ,完善的测试方案能让程序更加健壮与安全。
将于 2022 年 2 月发布的 Go 1.18 是一个大版本:除了备受关注的泛型之外,还有一个非常大的特性将会加入,它就是模糊测试(Fuzzing test)。
撰写测试代码能够验证功能特性,提前发现程序错误。我们在设计测试用例时,一般会采用错误猜测增加一些异常用例,检测被测对象在非正常情况下处理异常的能力,以发现被测对象中潜在的缺陷。
但是,采用这种测试设计会面临一个问题:测试用例通常是模糊的。为什么这么说呢?这里列几个原因:
举个例子。假设有一个功能函数,它接收两个整数输入并做一些处理逻辑。那么我们会如何写测试用例呢?
朴素的做法是为期望函数处理的每个数字编写一个测试用例。如果用例数据有 [-100,0]、[222,33]、[-1212,22],此时就有了三个测试用例。但很明显这里漏了一大类情况,例如都是负数?
更有甚者,由于处理逻辑的特殊性,测试用例仅在输入为 [2323,444] 时发生异常,而其他输入数字都正常工作。这应该怎么测试覆盖?
开发人员无法测试所有可能的输入值。此时,模糊测试就体现出了价值。
模糊测试是一种自动化测试技术,其依赖随机函数生成测试用例,并将这些用例不断地输入程序进行测试,以发现代码可能受到影响的问题。
模糊测试的用例是不重复、随机的,这能带给我们意想不到的输入和结果,可以达到我们错过的边缘情况。
关于模糊测试,早在 2017 年就已经有了提案(详见 issu #19109、#44551)。Go 模糊测试开源库 go-fuzz 作者 dvyukov 和 @kcc 建议 cmd/go 原生支持模糊测试,就像 tests、benchmarks 和 race detection 一样,是 Go 的一等公民。
提案得到了 Go 官方的支持,今年 6 月已将模糊测试置于 tip 上进行 beat 测试。若无意外,模糊测试将于 1.18 版本得到正式发布。
接下来,我们使用 gotip 来尝鲜模糊测试
$ go install golang.org/dl/gotip@latest
$ gotip download
安装完毕后,gotip 作为 go 命令的替代品,现在可以运行以下命令进行模糊测试
$ gotip test -fuzz=Fuzz
与单元测试一样,模糊测试代码必须写在 *_test.go
的文件中,测试函数命名形如 FuzzXxx
。该函数必须传递一个 *testing.F
参数,就像传递给 TestXxx
函数的 *testing.T
参数一样。
func FuzzXXX(f *testing.F) {
f.Fuzz(func(t *testing.T, m string, n int) {
err := doSomething(m, n)
// check for errors in err here
})
}
模糊测试模板函数形如上述代码。在第二行中,我们通过 f.Fuzz
函数填充一个常规的测试函数,测试函数包括了 *testing.T
类型的 t
,功能函数 doSomething
所需要的 string
类型的 m
与 int
类型的 n
。
Go 将会自动生成 m
与 n
参数的随机测试用例。
下面是一个 Go 官网博客给出的测试 net/url
包函数的模糊测试示例。
//go:build go1.18
// +build go1.18
package fuzz
import (
"net/url"
"reflect"
"testing"
)
func FuzzParseQuery(f *testing.F) {
f.Add("x=1&y=2")
f.Fuzz(func(t *testing.T, queryStr string) {
query, err := url.ParseQuery(queryStr)
if err != nil {
t.Skip()
}
queryStr2 := query.Encode()
query2, err := url.ParseQuery(queryStr2)
if err != nil {
t.Fatalf("ParseQuery failed to decode a valid encoded query %s: %v", queryStr2, err)
}
if !reflect.DeepEqual(query, query2) {
t.Errorf("ParseQuery gave different query after being encoded\nbefore: %v\nafter: %v", query, query2)
}
})
}
其中 queryStr
参数为 string
类型,这代表模糊测试随机生成的输入用例类型。
测试结果
模糊测试
模糊测试会大量消耗机器资源。在默认情况下,它启用 GOMAXPROCS 个 worker 并行运行模糊测试,且永不停止。
我们可以通过以下运行参数,对模糊测试进行控制。
-fuzz name
Run the fuzz test with the given regexp. Must match at most one fuzz
test.
-fuzztime
Run enough iterations of the fuzz test to take t, specified as a
time.Duration (for example, -fuzztime 1h30s).
The default is to run forever.
The special syntax Nx means to run the fuzz test N times
(for example, -fuzztime 100x).
-keepfuzzing
Keep running the fuzz test if a crasher is found. (default false)
-parallel
Allow parallel execution of a fuzz target that calls t.Parallel when
running the seed corpus.
While fuzzing with -fuzz, the value of this flag is the maximum number of
workers to run the fuzz target simultaneously; by default, it is set to
the value of GOMAXPROCS.
Note that -parallel only applies within a single test binary.
-race
Enable data race detection while fuzzing. (default false)
-run
Run only those tests, examples, and fuzz tests matching the regular
expression.
For testing a single seed corpus entry for a fuzz test, the regular
expression can be in the form $test/$name, where $test is the name of
the fuzz test, and $name is the name of the file (ignoring file
extensions) to run.
模糊测试是安全领域内常见的技术,黑客们常使用类似的方法查找系统漏洞。
在测试设计中,我们人工提供的测试用例总是有限的,而错误很可能就发生在被忽略的角落。模糊测试通过自动生成随机数据输入到程序中,监视程序运行情况以发现可能的发生的错误。
在 Go 开源领域,go-fuzz 是一个非常不错的模糊测试工具(感兴趣可以通过参考链接自行学习),但是它没有像使用原生的 Go 工具,例如竞态检测和基准测试一样,那么方便灵活。
目前 Go 原生支持的模糊测试已经 beta 测试了,并将于 Go 1.18 版本发行。这是一个和泛型一样值得期待的功能,两者共同改变着我们开发和测试 Go 程序的方式,让我们拭目以待。
Fuzzing is Beta Ready https://go.dev/blog/fuzz-beta
testing: add fuzz test support https://github.com/golang/go/issues/44551
Design Draft: First Class Fuzzing https://go.googlesource.com/proposal/+/master/design/draft-fuzzing.md
proposal: cmd/go: make fuzzing a first class citizen, like tests or benchmarks https://github.com/golang/go/issues/19109
go-fuzz https://github.com/dvyukov/go-fuzz
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8