Skip to content

模糊测试

什么是模糊测试?

模糊测试(Fuzzing)是一种自动化测试技术,通过向程序输入随机或半随机的数据来发现潜在的漏洞和错误。Go 1.18+ 原生支持模糊测试,可以帮助发现以下问题:

  • SQL注入攻击
  • 缓冲区溢出
  • 拒绝服务攻击
  • 跨站脚本攻击
  • 程序崩溃等

模糊测试的工作原理

  1. 种子语料库:提供初始测试数据
  2. 随机生成:基于种子数据生成大量变异输入
  3. 覆盖率引导:优先测试能增加代码覆盖率的输入
  4. 错误检测:自动发现导致程序崩溃或错误的输入

快速入门示例

1. 创建项目结构

bash
mkdir fuzz-demo
cd fuzz-demo
go mod init example/fuzz

2. 编写待测试函数

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

调试模糊测试失败

当模糊测试发现错误时:

  1. 查看失败输入:Go会保存导致失败的输入到 testdata/fuzz/ 目录
  2. 重现错误:使用保存的输入重新运行测试
  3. 分析根因:检查为什么特定输入导致失败
  4. 修复代码:修复发现的bug
  5. 验证修复:重新运行模糊测试确认修复

实际应用场景

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的强大工具。通过自动生成大量测试输入,它能够发现传统测试难以覆盖的边界情况和异常路径。

关键要点:

  1. 模糊测试补充而非替代单元测试
  2. 选择处理外部输入的函数进行模糊测试
  3. 提供多样化的种子语料库
  4. 验证函数应该满足的属性
  5. 及时修复发现的问题

通过合理使用模糊测试,可以显著提高代码的健壮性和安全性。

Released under the MIT License.