单元测试只能测试你想到的情况,而模糊测试能帮你发现你没想到的边界情况。

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, []byte
  • int, int8, int16, int32, int64
  • uint, uint8, uint16, uint32, uint64
  • float32, float64
  • bool
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 测试

思考题: 为什么模糊测试不能替代单元测试?两者应该如何配合使用?


相关阅读