模糊测试
什么是模糊测试?
模糊测试(Fuzzing)是一种自动化测试技术,通过向程序输入随机或半随机的数据来发现潜在的漏洞和错误。Go 1.18+ 原生支持模糊测试,可以帮助发现以下问题:
- SQL注入攻击
- 缓冲区溢出
- 拒绝服务攻击
- 跨站脚本攻击
- 程序崩溃等
模糊测试的工作原理
- 种子语料库:提供初始测试数据
- 随机生成:基于种子数据生成大量变异输入
- 覆盖率引导:优先测试能增加代码覆盖率的输入
- 错误检测:自动发现导致程序崩溃或错误的输入
快速入门示例
1. 创建项目结构
bash
mkdir fuzz-demo
cd fuzz-demo
go mod init example/fuzz2. 编写待测试函数
创建 main.go 文件:
go
package main
import (
"errors"
"fmt"
"unicode/utf8"
)
// Reverse 反转字符串函数
func Reverse(s string) (string, error) {
if !utf8.ValidString(s) {
return s, errors.New("input is not valid UTF-8")
}
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r), nil
}
func main() {
input := "Hello, 世界"
rev, err := Reverse(input)
if err != nil {
fmt.Printf("错误: %v\n", err)
return
}
fmt.Printf("原始: %q\n", input)
fmt.Printf("反转: %q\n", rev)
}3. 编写单元测试
创建 reverse_test.go 文件:
go
package main
import (
"testing"
"unicode/utf8"
)
// 传统单元测试
func TestReverse(t *testing.T) {
testcases := []struct {
in, want string
}{
{"Hello, world", "dlrow ,olleH"},
{" ", " "},
{"!12345", "54321!"},
{"Hello, 世界", "界世 ,olleH"},
}
for _, tc := range testcases {
rev, err := Reverse(tc.in)
if err != nil {
t.Errorf("Reverse(%q) 返回错误: %v", tc.in, err)
continue
}
if rev != tc.want {
t.Errorf("Reverse(%q) = %q, 期望 %q", tc.in, rev, tc.want)
}
}
}4. 编写模糊测试
在 reverse_test.go 中添加模糊测试:
go
// 模糊测试函数
func FuzzReverse(f *testing.F) {
// 提供种子语料库
testcases := []string{
"Hello, world",
" ",
"!12345",
"Hello, 世界",
"🚀🌟",
}
for _, tc := range testcases {
f.Add(tc) // 添加种子数据
}
// 模糊测试逻辑
f.Fuzz(func(t *testing.T, orig string) {
// 第一次反转
rev, err1 := Reverse(orig)
if err1 != nil {
return // 跳过无效输入
}
// 第二次反转(应该恢复原样)
doubleRev, err2 := Reverse(rev)
if err2 != nil {
return
}
// 验证:两次反转应该恢复原始字符串
if orig != doubleRev {
t.Errorf("双重反转失败: 原始=%q, 结果=%q", orig, doubleRev)
}
// 验证:如果输入是有效UTF-8,输出也应该是
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("反转产生了无效的UTF-8字符串: %q", rev)
}
})
}运行模糊测试
基本命令
bash
# 运行普通测试
go test
# 运行模糊测试(无限期运行直到发现错误)
go test -fuzz=Fuzz
# 运行模糊测试30秒
go test -fuzz=Fuzz -fuzztime 30s
# 运行指定次数
go test -fuzz=Fuzz -fuzztime 100000x输出解读
fuzz: elapsed: 3s, execs: 86342 (28778/sec), new interesting: 2 (total: 35)elapsed: 运行时间execs: 执行次数28778/sec: 每秒执行次数new interesting: 新发现的有趣输入(扩展代码覆盖率)total: 总的有趣输入数量
模糊测试最佳实践
1. 选择合适的函数
适合模糊测试的函数特征:
- 处理外部输入
- 解析数据格式
- 字符串处理
- 数学计算
- 网络协议处理
2. 提供好的种子语料库
go
func FuzzParseURL(f *testing.F) {
// 提供多样化的种子数据
seeds := []string{
"https://example.com",
"http://localhost:8080/path?query=value",
"ftp://user:pass@host.com/file",
"mailto:user@example.com",
"", // 边界情况
"invalid-url",
}
for _, seed := range seeds {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, input string) {
// 测试逻辑
})
}3. 处理错误情况
go
f.Fuzz(func(t *testing.T, input string) {
result, err := ProcessInput(input)
if err != nil {
// 对于预期的错误,直接返回
return
}
// 验证结果的正确性
if !isValidResult(result) {
t.Errorf("无效结果: %v", result)
}
})4. 属性测试
验证函数应该满足的属性:
go
func FuzzSort(f *testing.F) {
f.Fuzz(func(t *testing.T, data []int) {
original := make([]int, len(data))
copy(original, data)
Sort(data)
// 属性1: 长度不变
if len(data) != len(original) {
t.Error("排序后长度改变")
}
// 属性2: 元素有序
for i := 1; i < len(data); i++ {
if data[i-1] > data[i] {
t.Error("排序结果无序")
}
}
// 属性3: 包含相同元素
if !sameElements(data, original) {
t.Error("排序后元素不匹配")
}
})
}支持的数据类型
Go模糊测试目前支持以下内置类型:
string[]byteint,int8,int16,int32,int64uint,uint8,uint16,uint32,uint64float32,float64bool
调试模糊测试失败
当模糊测试发现错误时:
- 查看失败输入:Go会保存导致失败的输入到
testdata/fuzz/目录 - 重现错误:使用保存的输入重新运行测试
- 分析根因:检查为什么特定输入导致失败
- 修复代码:修复发现的bug
- 验证修复:重新运行模糊测试确认修复
实际应用场景
JSON解析器测试
go
func FuzzJSONParser(f *testing.F) {
f.Add(`{"name": "test", "value": 123}`)
f.Add(`[]`)
f.Add(`null`)
f.Fuzz(func(t *testing.T, jsonStr string) {
var result interface{}
err := json.Unmarshal([]byte(jsonStr), &result)
if err != nil {
return // 无效JSON,跳过
}
// 验证重新编码后一致性
encoded, err := json.Marshal(result)
if err != nil {
t.Errorf("重新编码失败: %v", err)
}
var result2 interface{}
if err := json.Unmarshal(encoded, &result2); err != nil {
t.Errorf("重新解析失败: %v", err)
}
})
}URL解析器测试
go
func FuzzURLParser(f *testing.F) {
f.Add("https://example.com/path?query=value#fragment")
f.Add("http://localhost:8080")
f.Fuzz(func(t *testing.T, rawURL string) {
parsed, err := url.Parse(rawURL)
if err != nil {
return
}
// 验证重新构建URL的一致性
rebuilt := parsed.String()
reparsed, err := url.Parse(rebuilt)
if err != nil {
t.Errorf("重新解析失败: %v", err)
}
if parsed.Host != reparsed.Host {
t.Errorf("Host不匹配: %s vs %s", parsed.Host, reparsed.Host)
}
})
}总结
模糊测试是发现代码中隐藏bug的强大工具。通过自动生成大量测试输入,它能够发现传统测试难以覆盖的边界情况和异常路径。
关键要点:
- 模糊测试补充而非替代单元测试
- 选择处理外部输入的函数进行模糊测试
- 提供多样化的种子语料库
- 验证函数应该满足的属性
- 及时修复发现的问题
通过合理使用模糊测试,可以显著提高代码的健壮性和安全性。
CodeVortex