单元测试只能测试你想到的情况,而模糊测试能帮你发现你没想到的边界情况。
1. 什么是模糊测试?
模糊测试 (Fuzzing) 是一种自动化测试技术,通过生成大量随机或半随机的输入数据来测试程序,寻找崩溃、panic、死循环等异常。
传统测试 vs 模糊测试:
// 传统单元测试:测试已知的输入
func TestAdd(t *testing.T) {
if Add(2, 3) != 5 {
t.Error("2 + 3 should be 5")
}
}
// 模糊测试:测试大量随机输入
func FuzzAdd(f *testing.F) {
f.Fuzz(func(t *testing.T, a, b int) {
result := Add(a, b)
// 检查属性而不是具体值
if result < a && result < b {
t.Errorf("Add(%d, %d) = %d, should be >= both", a, b, result)
}
})
}
2. 编写 Fuzz 测试
2.1 基础示例
假设我们有一个解析 URL 的函数:
// url.go
package myurl
import (
"fmt"
"strings"
)
func ParseURL(rawURL string) (string, error) {
if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") {
return "", fmt.Errorf("invalid protocol")
}
return rawURL, nil
}
编写 Fuzz 测试:
// url_test.go
package myurl
import (
"testing"
)
func FuzzParseURL(f *testing.F) {
// 添加种子语料库(可选)
f.Add("http://example.com")
f.Add("https://google.com")
f.Add("ftp://invalid.com")
// Fuzz 函数
f.Fuzz(func(t *testing.T, rawURL string) {
result, err := ParseURL(rawURL)
// 检查:不应该 panic
// 检查:如果没有错误,结果应该等于输入
if err == nil && result != rawURL {
t.Errorf("ParseURL(%q) = %q, want %q", rawURL, result, rawURL)
}
})
}
2.2 运行 Fuzz 测试
# 运行模糊测试(会一直运行直到发现问题或手动停止)
go test -fuzz=FuzzParseURL
# 限制运行时间
go test -fuzz=FuzzParseURL -fuzztime=30s
# 限制迭代次数
go test -fuzz=FuzzParseURL -fuzztime=10000x
2.3 查看结果
如果发现问题,Go 会自动保存导致崩溃的输入:
testdata/fuzz/FuzzParseURL/
├── 1234567890abcdef # 导致问题的输入
└── ...
3. 支持的类型
Fuzz 函数的参数只能是以下类型:
string,[]byteint,int8,int16,int32,int64uint,uint8,uint16,uint32,uint64float32,float64bool
func FuzzMultipleTypes(f *testing.F) {
f.Add("hello", 42, true)
f.Fuzz(func(t *testing.T, s string, n int, b bool) {
// 测试逻辑
})
}
4. 实战示例
4.1 测试 JSON 解析
func FuzzJSONParse(f *testing.F) {
f.Add(`{"name":"Alice","age":30}`)
f.Add(`{"name":"Bob"}`)
f.Fuzz(func(t *testing.T, data string) {
var result map[string]interface{}
// 不应该 panic
json.Unmarshal([]byte(data), &result)
// 如果解析成功,再次序列化应该不出错
if result != nil {
_, err := json.Marshal(result)
if err != nil {
t.Errorf("Failed to re-marshal: %v", err)
}
}
})
}
4.2 测试字符串处理
func Reverse(s string) string {
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes)
}
func FuzzReverse(f *testing.F) {
f.Add("hello")
f.Add("世界")
f.Fuzz(func(t *testing.T, s string) {
// 属性:反转两次应该等于原字符串
reversed := Reverse(s)
doubleReversed := Reverse(reversed)
if s != doubleReversed {
t.Errorf("Reverse(Reverse(%q)) = %q, want %q", s, doubleReversed, s)
}
})
}
5. 最佳实践
5.1 添加种子语料库
func FuzzMyFunc(f *testing.F) {
// 添加已知的边界情况
f.Add("") // 空字符串
f.Add("a") // 单字符
f.Add("hello world") // 正常情况
f.Add(strings.Repeat("x", 10000)) // 超长字符串
f.Fuzz(func(t *testing.T, input string) {
// ...
})
}
5.2 检查属性而非具体值
// ❌ 不好:检查具体值
f.Fuzz(func(t *testing.T, a, b int) {
if Add(a, b) != a+b { // 这和单元测试没区别
t.Error("wrong")
}
})
// ✅ 好:检查属性
f.Fuzz(func(t *testing.T, a, b int) {
result := Add(a, b)
// 属性:结果应该大于等于两个输入
if a >= 0 && b >= 0 && result < a {
t.Error("result should be >= a")
}
})
5.3 避免 panic
func FuzzDivide(f *testing.F) {
f.Fuzz(func(t *testing.T, a, b int) {
// 避免除零 panic
if b == 0 {
return
}
result := a / b
// 检查属性
})
}
6. 持续集成中的 Fuzz 测试
# 在 CI 中运行短时间的 Fuzz 测试
go test -fuzz=. -fuzztime=10s
# 或者只运行回归测试(使用已发现的崩溃输入)
go test
7. 总结
- 模糊测试能发现边界情况的 Bug
- 使用
f.Add()添加种子语料库 - 检查属性而非具体值
- 在 CI 中运行短时间的 Fuzz 测试
思考题: 为什么模糊测试不能替代单元测试?两者应该如何配合使用?