1.10.2 模糊测试 #
模糊测试(Fuzzing)是 Go 1.18 引入的一个强大的测试特性,它能够自动生成大量的随机输入来测试函数,帮助发现边界情况和潜在的 bug。模糊测试是一种自动化测试技术,通过提供意外的、随机的或格式错误的输入来发现程序中的漏洞和错误。
模糊测试的基本原理 #
什么是模糊测试 #
模糊测试是一种软件测试技术,它通过向程序输入大量随机、无效或意外的数据来发现程序中的错误、崩溃或安全漏洞。Go 的模糊测试框架会:
- 生成测试输入:自动生成各种类型的输入数据
- 执行测试函数:使用生成的输入运行目标函数
- 监控执行结果:检测崩溃、panic 或其他异常行为
- 收集有趣的输入:保存能够触发新代码路径的输入
- 变异输入:基于有效输入生成新的测试用例
模糊测试的优势 #
// 传统单元测试的局限性
func TestParseEmail(t *testing.T) {
tests := []struct {
input string
expected bool
}{
{"[email protected]", true},
{"invalid-email", false},
{"", false},
{"@example.com", false},
{"user@", false},
}
for _, test := range tests {
result := IsValidEmail(test.input)
if result != test.expected {
t.Errorf("IsValidEmail(%q) = %v, want %v",
test.input, result, test.expected)
}
}
}
// 模糊测试可以发现更多边界情况
func FuzzParseEmail(f *testing.F) {
// 提供种子输入
f.Add("[email protected]")
f.Add("invalid-email")
f.Add("")
f.Fuzz(func(t *testing.T, email string) {
// 模糊测试不应该导致 panic
defer func() {
if r := recover(); r != nil {
t.Errorf("IsValidEmail panicked with input %q: %v", email, r)
}
}()
// 执行函数,确保不会崩溃
IsValidEmail(email)
})
}
Go 内置模糊测试框架 #
基本语法 #
Go 的模糊测试函数必须:
- 以
Fuzz
开头 - 接受
*testing.F
参数 - 位于
*_test.go
文件中
package main
import (
"fmt"
"regexp"
"strconv"
"strings"
"testing"
"unicode"
)
// 待测试的函数:验证邮箱格式
func IsValidEmail(email string) bool {
if email == "" {
return false
}
// 简单的邮箱验证正则表达式
pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
matched, err := regexp.MatchString(pattern, email)
if err != nil {
return false
}
return matched
}
// 模糊测试邮箱验证函数
func FuzzIsValidEmail(f *testing.F) {
// 添加种子输入
f.Add("[email protected]")
f.Add("[email protected]")
f.Add("invalid-email")
f.Add("@example.com")
f.Add("user@")
f.Add("")
f.Fuzz(func(t *testing.T, email string) {
// 确保函数不会 panic
defer func() {
if r := recover(); r != nil {
t.Errorf("IsValidEmail panicked with input %q: %v", email, r)
}
}()
result := IsValidEmail(email)
// 添加一些基本的不变性检查
if email == "" && result {
t.Errorf("Empty email should not be valid")
}
if !strings.Contains(email, "@") && result {
t.Errorf("Email without @ should not be valid: %q", email)
}
})
}
支持的数据类型 #
Go 模糊测试支持以下基本类型:
func FuzzMultipleTypes(f *testing.F) {
// 支持的基本类型
f.Add("string", int(42), int8(1), int16(2), int32(3), int64(4))
f.Add("test", uint(5), uint8(6), uint16(7), uint32(8), uint64(9))
f.Add("example", float32(3.14), float64(2.718), true)
f.Add("data", []byte("hello"))
f.Fuzz(func(t *testing.T, s string, i int, i8 int8, i16 int16, i32 int32, i64 int64,
ui uint, ui8 uint8, ui16 uint16, ui32 uint32, ui64 uint64,
f32 float32, f64 float64, b bool, data []byte) {
// 测试函数处理各种类型输入的能力
ProcessMultipleTypes(s, i, i8, i16, i32, i64, ui, ui8, ui16, ui32, ui64, f32, f64, b, data)
})
}
func ProcessMultipleTypes(s string, i int, i8 int8, i16 int16, i32 int32, i64 int64,
ui uint, ui8 uint8, ui16 uint16, ui32 uint32, ui64 uint64,
f32 float32, f64 float64, b bool, data []byte) {
// 处理各种类型的输入
fmt.Printf("String: %s, Int: %d, Bool: %t, Data: %s\n", s, i, b, string(data))
}
编写模糊测试用例 #
字符串处理函数的模糊测试 #
package stringutils
import (
"strings"
"unicode"
)
// 将字符串转换为标题格式
func ToTitle(s string) string {
if s == "" {
return ""
}
words := strings.Fields(s)
for i, word := range words {
if len(word) > 0 {
runes := []rune(word)
runes[0] = unicode.ToUpper(runes[0])
for j := 1; j < len(runes); j++ {
runes[j] = unicode.ToLower(runes[j])
}
words[i] = string(runes)
}
}
return strings.Join(words, " ")
}
// 反转字符串
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 IsPalindrome(s string) bool {
// 移除空格并转换为小写
cleaned := strings.ToLower(strings.ReplaceAll(s, " ", ""))
return cleaned == Reverse(cleaned)
}
// stringutils_test.go
package stringutils
import (
"strings"
"testing"
"unicode"
"unicode/utf8"
)
func FuzzToTitle(f *testing.F) {
// 添加种子输入
f.Add("hello world")
f.Add("HELLO WORLD")
f.Add("Hello World")
f.Add("")
f.Add("a")
f.Add("hello-world")
f.Add("hello_world")
f.Add("你好世界")
f.Fuzz(func(t *testing.T, input string) {
result := ToTitle(input)
// 不变性检查
if input == "" && result != "" {
t.Errorf("ToTitle of empty string should be empty, got %q", result)
}
// 检查结果是否为有效的 UTF-8
if !utf8.ValidString(result) {
t.Errorf("ToTitle produced invalid UTF-8: %q", result)
}
// 如果输入只包含空格,结果应该为空
if strings.TrimSpace(input) == "" && result != "" {
t.Errorf("ToTitle of whitespace-only string should be empty, got %q", result)
}
})
}
func FuzzReverse(f *testing.F) {
f.Add("hello")
f.Add("")
f.Add("a")
f.Add("ab")
f.Add("abc")
f.Add("你好")
f.Add("🙂🙃")
f.Fuzz(func(t *testing.T, input string) {
result := Reverse(input)
// 不变性检查
if len(input) != len(result) {
t.Errorf("Reverse changed string length: input %d, result %d",
len(input), len(result))
}
// 双重反转应该得到原字符串
doubleReverse := Reverse(result)
if input != doubleReverse {
t.Errorf("Double reverse failed: input %q, result %q", input, doubleReverse)
}
// 检查结果是否为有效的 UTF-8
if !utf8.ValidString(result) {
t.Errorf("Reverse produced invalid UTF-8: %q", result)
}
})
}
func FuzzIsPalindrome(f *testing.F) {
f.Add("racecar")
f.Add("A man a plan a canal Panama")
f.Add("race a car")
f.Add("")
f.Add("a")
f.Add("Aa")
f.Add("上海海上")
f.Fuzz(func(t *testing.T, input string) {
result := IsPalindrome(input)
// 空字符串应该是回文
if input == "" && !result {
t.Errorf("Empty string should be palindrome")
}
// 单字符应该是回文
if utf8.RuneCountInString(input) == 1 && !result {
t.Errorf("Single character should be palindrome: %q", input)
}
// 如果是回文,反转后应该仍然是回文
if result {
reversed := Reverse(input)
if !IsPalindrome(reversed) {
t.Errorf("Reversed palindrome should still be palindrome: %q -> %q",
input, reversed)
}
}
})
}
数值计算函数的模糊测试 #
package mathutils
import (
"errors"
"math"
)
// 计算平方根
func Sqrt(x float64) (float64, error) {
if x < 0 {
return 0, errors.New("cannot compute square root of negative number")
}
if x == 0 {
return 0, nil
}
// 使用牛顿法计算平方根
guess := x / 2
for i := 0; i < 10; i++ {
guess = (guess + x/guess) / 2
}
return guess, nil
}
// 计算阶乘
func Factorial(n int) (int64, error) {
if n < 0 {
return 0, errors.New("factorial of negative number is undefined")
}
if n > 20 {
return 0, errors.New("factorial too large")
}
if n == 0 || n == 1 {
return 1, nil
}
result := int64(1)
for i := 2; i <= n; i++ {
result *= int64(i)
}
return result, nil
}
// 最大公约数
func GCD(a, b int) int {
if a < 0 {
a = -a
}
if b < 0 {
b = -b
}
for b != 0 {
a, b = b, a%b
}
return a
}
// mathutils_test.go
package mathutils
import (
"math"
"testing"
)
func FuzzSqrt(f *testing.F) {
f.Add(4.0)
f.Add(9.0)
f.Add(0.0)
f.Add(1.0)
f.Add(2.0)
f.Add(100.0)
f.Fuzz(func(t *testing.T, x float64) {
result, err := Sqrt(x)
if x < 0 {
// 负数应该返回错误
if err == nil {
t.Errorf("Sqrt of negative number should return error: %f", x)
}
return
}
if err != nil {
t.Errorf("Sqrt of non-negative number should not return error: %f", x)
return
}
// 检查结果的正确性(允许小的浮点误差)
if x == 0 && result != 0 {
t.Errorf("Sqrt(0) should be 0, got %f", result)
}
if x > 0 {
// 验证 result * result ≈ x
square := result * result
diff := math.Abs(square - x)
tolerance := 1e-10
if diff > tolerance {
t.Errorf("Sqrt(%f) = %f, but %f^2 = %f (diff: %f)",
x, result, result, square, diff)
}
}
// 结果不应该是 NaN 或无穷大
if math.IsNaN(result) || math.IsInf(result, 0) {
t.Errorf("Sqrt(%f) returned invalid result: %f", x, result)
}
})
}
func FuzzFactorial(f *testing.F) {
f.Add(0)
f.Add(1)
f.Add(5)
f.Add(10)
f.Add(20)
f.Fuzz(func(t *testing.T, n int) {
result, err := Factorial(n)
if n < 0 {
if err == nil {
t.Errorf("Factorial of negative number should return error: %d", n)
}
return
}
if n > 20 {
if err == nil {
t.Errorf("Factorial of large number should return error: %d", n)
}
return
}
if err != nil {
t.Errorf("Factorial of valid number should not return error: %d", n)
return
}
// 检查基本情况
if n == 0 || n == 1 {
if result != 1 {
t.Errorf("Factorial(%d) should be 1, got %d", n, result)
}
}
// 检查递归性质:n! = n * (n-1)!
if n > 1 {
prevFactorial, prevErr := Factorial(n - 1)
if prevErr != nil {
t.Errorf("Failed to compute factorial of %d", n-1)
return
}
expected := int64(n) * prevFactorial
if result != expected {
t.Errorf("Factorial(%d) = %d, expected %d", n, result, expected)
}
}
// 结果应该是正数
if result <= 0 {
t.Errorf("Factorial(%d) should be positive, got %d", n, result)
}
})
}
func FuzzGCD(f *testing.F) {
f.Add(12, 8)
f.Add(0, 5)
f.Add(5, 0)
f.Add(1, 1)
f.Add(-12, 8)
f.Add(12, -8)
f.Fuzz(func(t *testing.T, a, b int) {
result := GCD(a, b)
// GCD 应该是非负数
if result < 0 {
t.Errorf("GCD(%d, %d) should be non-negative, got %d", a, b, result)
}
// GCD(0, 0) 应该是 0
if a == 0 && b == 0 && result != 0 {
t.Errorf("GCD(0, 0) should be 0, got %d", result)
}
// GCD(a, 0) 应该是 |a|
if b == 0 && a != 0 {
expected := a
if expected < 0 {
expected = -expected
}
if result != expected {
t.Errorf("GCD(%d, 0) should be %d, got %d", a, expected, result)
}
}
// GCD(0, b) 应该是 |b|
if a == 0 && b != 0 {
expected := b
if expected < 0 {
expected = -expected
}
if result != expected {
t.Errorf("GCD(0, %d) should be %d, got %d", b, expected, result)
}
}
// 交换律:GCD(a, b) = GCD(b, a)
if GCD(b, a) != result {
t.Errorf("GCD should be commutative: GCD(%d, %d) != GCD(%d, %d)",
a, b, b, a)
}
// 如果 a 和 b 都不为 0,GCD 应该能整除 a 和 b
if a != 0 && b != 0 && result > 0 {
if a%result != 0 {
t.Errorf("GCD(%d, %d) = %d should divide %d", a, b, result, a)
}
if b%result != 0 {
t.Errorf("GCD(%d, %d) = %d should divide %d", a, b, result, b)
}
}
})
}
模糊测试的实际应用 #
JSON 解析器的模糊测试 #
package jsonparser
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
)
// 简单的 JSON 解析器
type SimpleJSON struct {
data interface{}
}
func Parse(jsonStr string) (*SimpleJSON, error) {
var data interface{}
err := json.Unmarshal([]byte(jsonStr), &data)
if err != nil {
return nil, fmt.Errorf("failed to parse JSON: %w", err)
}
return &SimpleJSON{data: data}, nil
}
func (sj *SimpleJSON) GetString(key string) (string, error) {
obj, ok := sj.data.(map[string]interface{})
if !ok {
return "", errors.New("not a JSON object")
}
value, exists := obj[key]
if !exists {
return "", fmt.Errorf("key %q not found", key)
}
str, ok := value.(string)
if !ok {
return "", fmt.Errorf("value for key %q is not a string", key)
}
return str, nil
}
func (sj *SimpleJSON) GetInt(key string) (int, error) {
obj, ok := sj.data.(map[string]interface{})
if !ok {
return 0, errors.New("not a JSON object")
}
value, exists := obj[key]
if !exists {
return 0, fmt.Errorf("key %q not found", key)
}
switch v := value.(type) {
case float64:
return int(v), nil
case int:
return v, nil
case string:
return strconv.Atoi(v)
default:
return 0, fmt.Errorf("value for key %q cannot be converted to int", key)
}
}
// jsonparser_test.go
package jsonparser
import (
"encoding/json"
"strings"
"testing"
)
func FuzzJSONParse(f *testing.F) {
// 添加有效的 JSON 种子
f.Add(`{"name": "John", "age": 30}`)
f.Add(`{"items": [1, 2, 3]}`)
f.Add(`{"nested": {"key": "value"}}`)
f.Add(`[]`)
f.Add(`{}`)
f.Add(`null`)
f.Add(`true`)
f.Add(`false`)
f.Add(`42`)
f.Add(`"string"`)
// 添加一些无效的 JSON 种子
f.Add(`{`)
f.Add(`}`)
f.Add(`{"key": }`)
f.Add(`{"key": "value",}`)
f.Fuzz(func(t *testing.T, jsonStr string) {
sj, err := Parse(jsonStr)
// 检查标准库的行为一致性
var stdData interface{}
stdErr := json.Unmarshal([]byte(jsonStr), &stdData)
if stdErr != nil {
// 如果标准库解析失败,我们的解析器也应该失败
if err == nil {
t.Errorf("Standard library failed to parse %q, but our parser succeeded", jsonStr)
}
return
}
// 如果标准库解析成功,我们的解析器也应该成功
if err != nil {
t.Errorf("Standard library parsed %q successfully, but our parser failed: %v",
jsonStr, err)
return
}
// 测试 GetString 和 GetInt 方法不会 panic
defer func() {
if r := recover(); r != nil {
t.Errorf("JSON methods panicked with input %q: %v", jsonStr, r)
}
}()
// 尝试获取一些常见的键
commonKeys := []string{"name", "age", "id", "value", "key", "data"}
for _, key := range commonKeys {
sj.GetString(key)
sj.GetInt(key)
}
})
}
func FuzzJSONGetString(f *testing.F) {
f.Add(`{"name": "John"}`, "name")
f.Add(`{"age": 30}`, "age")
f.Add(`{}`, "missing")
f.Add(`[]`, "key")
f.Fuzz(func(t *testing.T, jsonStr, key string) {
sj, err := Parse(jsonStr)
if err != nil {
return // 跳过无效的 JSON
}
// GetString 不应该 panic
defer func() {
if r := recover(); r != nil {
t.Errorf("GetString panicked with JSON %q and key %q: %v",
jsonStr, key, r)
}
}()
result, err := sj.GetString(key)
// 如果成功获取到字符串,验证它确实是字符串
if err == nil && result == "" && !strings.Contains(jsonStr, `"`+key+`": ""`) {
// 空字符串应该只在 JSON 中确实包含空字符串时返回
if !strings.Contains(jsonStr, `"`+key+`": ""`) {
t.Logf("Got empty string for key %q in JSON %q", key, jsonStr)
}
}
})
}
URL 解析器的模糊测试 #
package urlparser
import (
"errors"
"net/url"
"strings"
)
// 简单的 URL 解析器
type URL struct {
Scheme string
Host string
Port string
Path string
Query map[string]string
Fragment string
}
func ParseURL(rawURL string) (*URL, error) {
if rawURL == "" {
return nil, errors.New("empty URL")
}
// 使用标准库解析
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
// 解析查询参数
query := make(map[string]string)
for key, values := range u.Query() {
if len(values) > 0 {
query[key] = values[0]
}
}
return &URL{
Scheme: u.Scheme,
Host: u.Hostname(),
Port: u.Port(),
Path: u.Path,
Query: query,
Fragment: u.Fragment,
}, nil
}
func (u *URL) String() string {
var result strings.Builder
if u.Scheme != "" {
result.WriteString(u.Scheme)
result.WriteString("://")
}
if u.Host != "" {
result.WriteString(u.Host)
if u.Port != "" {
result.WriteString(":")
result.WriteString(u.Port)
}
}
result.WriteString(u.Path)
if len(u.Query) > 0 {
result.WriteString("?")
first := true
for key, value := range u.Query {
if !first {
result.WriteString("&")
}
result.WriteString(key)
result.WriteString("=")
result.WriteString(value)
first = false
}
}
if u.Fragment != "" {
result.WriteString("#")
result.WriteString(u.Fragment)
}
return result.String()
}
// urlparser_test.go
package urlparser
import (
"net/url"
"strings"
"testing"
)
func FuzzParseURL(f *testing.F) {
// 添加有效的 URL 种子
f.Add("https://example.com")
f.Add("http://example.com:8080/path?key=value#fragment")
f.Add("ftp://user:[email protected]/file.txt")
f.Add("/relative/path")
f.Add("?query=only")
f.Add("#fragment-only")
f.Add("mailto:[email protected]")
// 添加一些边界情况
f.Add("")
f.Add("://")
f.Add("http://")
f.Add("example.com")
f.Fuzz(func(t *testing.T, rawURL string) {
parsedURL, err := ParseURL(rawURL)
// 与标准库行为对比
stdURL, stdErr := url.Parse(rawURL)
if rawURL == "" {
// 空 URL 应该返回错误
if err == nil {
t.Errorf("Empty URL should return error")
}
return
}
if stdErr != nil {
// 如果标准库解析失败,我们也应该失败
if err == nil {
t.Errorf("Standard library failed to parse %q, but our parser succeeded", rawURL)
}
return
}
if err != nil {
t.Errorf("Failed to parse valid URL %q: %v", rawURL, err)
return
}
// 验证解析结果的一致性
if parsedURL.Scheme != stdURL.Scheme {
t.Errorf("Scheme mismatch for %q: got %q, want %q",
rawURL, parsedURL.Scheme, stdURL.Scheme)
}
if parsedURL.Host != stdURL.Hostname() {
t.Errorf("Host mismatch for %q: got %q, want %q",
rawURL, parsedURL.Host, stdURL.Hostname())
}
if parsedURL.Port != stdURL.Port() {
t.Errorf("Port mismatch for %q: got %q, want %q",
rawURL, parsedURL.Port, stdURL.Port())
}
// 测试 String 方法不会 panic
defer func() {
if r := recover(); r != nil {
t.Errorf("String method panicked with URL %q: %v", rawURL, r)
}
}()
urlString := parsedURL.String()
// 重新解析生成的字符串应该成功
if urlString != "" {
_, repParseErr := ParseURL(urlString)
if repParseErr != nil {
t.Errorf("Failed to re-parse generated URL string %q: %v",
urlString, repParseErr)
}
}
})
}
运行模糊测试 #
# 运行单个模糊测试
go test -fuzz=FuzzParseURL
# 运行所有模糊测试
go test -fuzz=.
# 指定运行时间
go test -fuzz=FuzzParseURL -fuzztime=30s
# 指定最小化时间
go test -fuzz=FuzzParseURL -fuzzminimizetime=10s
# 查看模糊测试覆盖率
go test -fuzz=FuzzParseURL -coverprofile=fuzz.out
go tool cover -html=fuzz.out
模糊测试的最佳实践 #
- 提供好的种子输入:
func FuzzFunction(f *testing.F) {
// 提供各种边界情况作为种子
f.Add("") // 空输入
f.Add("normal input") // 正常输入
f.Add("边界情况") // Unicode 输入
f.Add(strings.Repeat("a", 1000)) // 长输入
}
- 检查不变性而非具体值:
f.Fuzz(func(t *testing.T, input string) {
result := ProcessString(input)
// 好:检查不变性
if len(input) > 0 && len(result) == 0 {
t.Error("Non-empty input should not produce empty result")
}
// 不好:检查具体值
// if result != "expected" { ... }
})
- 防止 panic:
f.Fuzz(func(t *testing.T, input string) {
defer func() {
if r := recover(); r != nil {
t.Errorf("Function panicked: %v", r)
}
}()
ProcessString(input)
})
- 限制资源使用:
f.Fuzz(func(t *testing.T, input string) {
// 限制输入大小
if len(input) > 10000 {
t.Skip("Input too large")
}
ProcessString(input)
})
通过本节的学习,您应该已经掌握了 Go 模糊测试的基本概念和实际应用。模糊测试是发现边界情况和潜在 bug 的强大工具,能够显著提高代码的健壮性和安全性。在下一节中,我们将学习 Go 1.16 引入的嵌入文件系统特性。