Go fuzzing模糊测试
fuzzing模糊测试在Go1.18中引入。官方文档:Tutorial: Getting started with fuzzing - The Go Programming Language,本文主要以官方文档的fuzzing入门教程为主,精简自认为重要的内容与自己的理解。因此相比官方文档略有简略,有步骤省略请查阅官方文档或参考资料中的翻译版本。
编写待测函数(写一个BUG)
在main.go
文件中写入函数内容:
func Reverse(s string) string {
b := []byte(s)
for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}
该函数功能较简单,可以翻转字符串。在main.go
中调用这个函数试试!ps:如上所述,内容较精简,只包含核心代码,如包声明,模块导入代码就省略了
func main() {
input := "The quick brown fox jumped over the lazy dog"
rev := Reverse(input)
doubleRev := Reverse(rev)
fmt.Printf("original: %q\n", input)
fmt.Printf("reversed: %q\n", rev)
fmt.Printf("reversed again: %q\n", doubleRev)
}
运行结果为
$ go run .
original: "The quick brown fox jumped over the lazy dog"
reversed: "god yzal eht revo depmuj xof nworb kciuq ehT"
reversed again: "The quick brown fox jumped over the lazy dog"
可以看到使用字符串"The quick brown fox jumped over the lazy dog"
作为函数的参数,函数正常执行且达到了预期效果。
单元测试(unit test)
创建reverse_test.go
文件编写单元测试函数。
package main
import (
"testing"
)
func TestReverse(t *testing.T) {
testcases := []struct {
in, want string
}{
{"Hello, world", "dlrow ,olleH"},
{" ", " "},
{"!12345", "54321!"},
}
for _, tc := range testcases {
rev := Reverse(tc.in)
if rev != tc.want {
t.Errorf("Reverse: %q, want %q", rev, tc.want)
}
}
}
使用go test
运行单元测试
$ go test
PASS
ok example/fuzz 0.013s
测试顺利通过。接下来将单元测试
改成模糊测试
(fuzzing)
模糊测试(fuzz test)
The unit test has limitations, namely that each input must be added to the test by the developer. One benefit of fuzzing is that it comes up with inputs for your code, and may identify edge cases that the test cases you came up with didn’t reach.
单元测试有局限性,每个测试输入必须由开发者指定加到单元测试的测试用例里。fuzzing的优点之一是可以基于开发者代码里指定的测试输入作为基础数据,进一步自动生成新的随机测试数据,用来发现指定测试输入没有覆盖到的边界情况。
注意:你可以把单元测试、性能测试(benchmarks)和模糊测试放在同一个*_test.go
文件里。
编写模糊测试
把reverse_test.go
里的单元测试代码TestReverse
替换成如下的模糊测试代码FuzzReverse
。
func FuzzReverse(f *testing.F) {
testcases := []string{"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc) // Use f.Add to provide a seed corpus
}
f.Fuzz(func(t *testing.T, orig string) {
rev := Reverse(orig)
doubleRev := Reverse(rev)
if orig != doubleRev {
t.Errorf("Before: %q, after: %q", orig, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
}
-
模糊测试也有一定的
局限性
。由于模糊测试的原理是将测试输入加入语料库
(corpus)后自动生成新的随机测试数据,因此我们无法预期输出结果。相反的在单元测试中预期输出结果是可知的。 -
由于预期结果未知,在该测试函数中我们只能对
Reverse
的两个特性进行验证-
对一个字符串做2次反转,得到的结果和源字符串相同
-
反转后的字符串也仍然是一个有效的UTF-8编码的字符串
-
注意:fuzzing模糊测试
和Go已有的单元测试
以及性能测试
框架是互为补充
的,并不是替代关系。
Go模糊测试和单元测试在语法上有如下差异:
- Go模糊测试函数以FuzzXxx开头,单元测试函数以TestXxx开头
- Go模糊测试函数以
*testing.F
作为入参,单元测试函数以*testing.T
作为入参 - Go模糊测试会调用
f.Add
函数和f.Fuzz
函数。f.Add
函数把指定输入作为模糊测试的种子语料库(seed corpus),fuzzing基于种子语料库生成随机输入。f.Fuzz
函数接收一个fuzz target函数作为入参。fuzz target函数有多个参数,第一个参数是*testing.T
,其它参数是被模糊的类型(注意:被模糊的类型目前只支持部分内置类型, 列在 Go Fuzzing docs,未来会支持更多的内置类型)。
运行模糊测试
-
执行如下命令来运行模糊测试。
这个方式只会使用种子语料库,而不会生成随机测试数据。通过这种方式可以用来验证种子语料库的测试数据是否可以测试通过。(fuzz test without fuzzing)
$ go test PASS ok example/fuzz 0.013s
如果
reverse_test.go
文件里有其它单元测试函数或者模糊测试函数,但是只想运行FuzzReverse
模糊测试函数,我们可以执行go test -run=FuzzReverse
命令。注意:
go test
默认会执行所有以TestXxx
开头的单元测试函数和以FuzzXxx
开头的模糊测试函数,默认不运行以BenchmarkXxx
开头的性能测试函数,如果我们想运行 benchmark用例,则需要加上-bench
参数。 -
如果要基于种子语料库生成随机测试数据用于模糊测试,需要给
go test
命令增加-fuzz
参数。(fuzz test with fuzzing)$ go test -fuzz=Fuzz fuzz: elapsed: 0s, gathering baseline coverage: 0/3 completed fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed, now fuzzing with 8 workers fuzz: minimizing 38-byte failing input file... --- FAIL: FuzzReverse (0.01s) --- FAIL: FuzzReverse (0.00s) reverse_test.go:20: Reverse produced invalid UTF-8 string "\x9c\xdd" Failing input written to testdata/fuzz/FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a To re-run: go test -run=FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a FAIL exit status 1 FAIL example/fuzz 0.030s
上面的fuzzing测试结果是
FAIL
,引起FAIL
的输入数据被写到了一个语料库文件里。下次运行go test
命令的时候,即使没有-fuzz
参数,这个语料库文件里的测试数据也会被用到。可以用文本编辑器打开
testdata/fuzz/FuzzReverse
目录下的文件,看看引起Fuzzing测试失败的测试数据长什么样。下面是一个示例文件,你那边运行后得到的测试数据可能和这个不一样,但文件里的内容格式会是一样的。go test fuzz v1 string("泃")
语料库文件里的第1行标识的是编码版本(encoding version,说直白点,就是这个种子语料库文件里内容格式的版本),虽然目前只有v1这1个版本,但是Fuzzing设计者考虑到未来可能引入新的编码版本,于是加了编码版本的概念。
从第2行开始,每一行数据对应的是语料库的每条测试数据(corpus entry)的其中一个参数,按照参数先后顺序排列。
f.Fuzz(func(t *testing.T, orig string) { rev := Reverse(orig) doubleRev := Reverse(rev) if orig != doubleRev { t.Errorf("Before: %q, after: %q", orig, doubleRev) } if utf8.ValidString(orig) && !utf8.ValidString(rev) { t.Errorf("Reverse produced invalid UTF-8 string %q %q", orig, rev) } })
本文的
FuzzReverse
里的fuzz target函数func(t *testing.T, orig string)
只有orig
这1个参数作为真正的测试输入,也就是每条测试数据其实就1个输入,因此在上面示例的testdata/fuzz/FuzzReverse
目录下的文件里只有string("泃")
这一行。如果每条测试数据有N个参数,那fuzzing找出的导致fuzz test失败的每条测试数据在
testdata
目录下的文件里会有N行,第i
行对应第i
个参数。 -
再次运行
go test
命令,这次不带-fuzz
参数。我们会发现虽然没有
-fuzz
参数,但是模糊测试的时候仍然用到了上面第2步找到的测试数据。$ go test --- FAIL: FuzzReverse (0.00s) --- FAIL: FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a (0.00s) reverse_test.go:20: Reverse produced invalid string FAIL exit status 1 FAIL example/fuzz 0.016s
既然Go fuzzing测试没通过,那就需要我们调试代码来找出问题所在了。
修BUG并反复运行模糊测试
该部分不是模糊测试的核心内容且内容较多。主要修了两个BUG,主要和[]byte
与[]rune
相关。具体内容详见:
- Tutorial: Getting started with fuzzing - The Go Programming Language
- 官方教程:Go fuzzing模糊测试 - SegmentFault 思否
补充一点:fuzz test如果没有遇到错误,默认会一直运行下去,需要使用 ctrl-C
结束测试。
也可以传递-fuzztime
参数来指定测试时间,这样就不用 ctrl-C
了。
总结
重点
- 单元测试和模糊测试的区别:
- 单元测试
可知
预期结果;单元测试测试输入由测试者提供,数量有限
。 - 模糊测试输入未知因此预期结果
不可知
;模糊测试中测试者只需将有限的测试输入使用t.Add
加入种子语料库,在模糊测试中即可自动生成若干测试输入
- 由上述差别导致单元测试和模糊测试的测试逻辑略有不同。
- 单元测试
go test
的使用go test
只会使用种子语料库
中的测试输入和以往测试中失败存放在文件中
的测试输入进行测试。go test -fuzz=Fuzz
则会基于种子语料库生成若干随机测试输入进行模糊测试。- 如在模糊测试中未遇到错误默认无限运行,需使用
CTRL+C
手动结束测试。当然,也可以在使用go test
时加入参数-fuzztime
参数指定测试时间。