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