Go 模糊测试
Go 1.18+ 内置 fuzzing:种子语料、变异输入、失败落盘与 corpus 管理,用于发现崩溃、死循环与不变量破坏。
[!info] 关联笔记
Go 模糊测试
这个概念为什么会出现
表驱动测试擅长已知边界:空串、最大长度、同事提过的坏例子。真实世界还有:
- 畸形 UTF-8、奇怪长度组合
- 解析器状态机的罕见转移
- “往返”不变量被冷门输入打破
模糊测试(fuzzing) 用引擎自动变异输入,寻找能让断言失败或 panic 的样本。Go 1.18 起 fuzz 进入标准 go test 工具链,无需先上外部框架即可对纯函数级目标开搞。
[!abstract] 一句话理解 Fuzz 测试提供种子语料并由引擎变异生成海量输入,反复执行目标函数;一旦失败就把输入沉淀到 corpus,变成可回归的资产。
最小可运行示例
先把示例放进业务场景,再看代码:
场景:为「展示名镜像排版」写往返不变量 fuzz
国际化站点要把用户展示名做「镜像翻转」用于某种 RTL 预览组件(实现上按 rune 反转)。
表驱动能覆盖空串、中文、emoji,但畸形 UTF-8、奇怪组合仍可能打破「翻转两次回到原文」。
用 fuzz:种子给业务常见名,引擎变异海量输入;一旦失败就落盘进 corpus,变成回归资产。
// displayname.go
package profile
// MirrorDisplayName 按 rune 反转展示名(RTL 预览用,演示实现)。
// 业务意图:预览组件需要可逆变换;教学点:纯函数 + 不变量最适合 fuzz。
func MirrorDisplayName(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
// displayname_fuzz_test.go
package profile
import "testing"
// FuzzMirrorDisplayName 守护「镜像两次 = 原文」等不变量。
//
// 业务意图:防止冷门 Unicode 把预览组件弄成不可逆乱码。
// 教学点:f.Add 种子 + f.Fuzz 目标;无 -fuzz 时仍跑种子/已存 corpus。
func FuzzMirrorDisplayName(f *testing.F) {
// 种子:真实展示名形态(英文、中文、空、emoji)。
f.Add("hello")
f.Add("世界")
f.Add("")
f.Add("🙂👍")
f.Fuzz(func(t *testing.T, in string) {
// 不变量 1:往返——预览开关两次应回到用户原名。
rev := MirrorDisplayName(in)
double := MirrorDisplayName(rev)
if double != in {
t.Fatalf("MirrorDisplayName(MirrorDisplayName(%q)) = %q", in, double)
}
// 不变量 2:rune 个数不变(避免把用户名「吃字符」)。
if len([]rune(rev)) != len([]rune(in)) {
t.Fatalf("rune length changed: in=%q rev=%q", in, rev)
}
})
}
建议运行:
# CI:只跑种子与已保存 corpus
go test .
# 本地/夜间:启用变异探索
go test -fuzz=FuzzMirrorDisplayName -fuzztime=30s .
期望:短时 fuzz 无失败时输出 PASS;若断言失败,失败输入会写入 testdata/fuzz/...,之后普通 go test 也会跑到。
结合场景再看三个关注点
-
结构:
FuzzXxx+f.Fuzz闭包
展示名目标的参数类型须在 fuzz 支持集合内(如string、[]byte)。 -
CI 与探索分工
默认go test回归 corpus;-fuzz才持续变异吃 CPU。 -
失败即资产
撞到的怪名字会落盘,避免「修过一次又丢」的国际化边角。
核心概念与准确模型
结构
种子 f.Add / testdata 语料
↓
变异引擎(实现细节)
↓
目标 f.Fuzz 闭包
↓
失败 → 最小化/保存 corpus
| 概念 | 含义 |
|---|---|
| Seed corpus | 你提供的初始有趣输入 |
| Generated corpus | 引擎发现的新覆盖/有趣输入 |
| Crashers | 导致失败的输入(会落盘) |
-fuzztime | 模糊持续时间或次数上限 |
与普通 Test 的关系
- 无
-fuzz:fuzz 目标仍执行种子与已保存 corpus——CI 友好 - 有
-fuzz:进入变异循环,吃 CPU
因此 fuzz 适合:
- CI:回归 corpus
- 开发者机器/夜间:探索
好的 fuzz 目标特征
适合:
- 纯函数、解析、编解码、压缩、校验
- 可表达不变量(往返、幂等、从不 panic、输出满足格式)
不适合单独:
- 强依赖外部网络/真实时钟且难注入
- 无断言的“跑一下”
- 非确定性且无法规范化的输出(需先固定随机源)
不变量示例
// 1) round-trip
// 2) 不 panic(由框架捕获)
// 3) 若 err==nil,则进一步结构合法
out, err := Parse(in)
if err != nil {
return // 拒绝合法
}
if err := out.Validate(); err != nil {
t.Fatal(err)
}
“拒绝非法输入”本身不是失败;接受后破坏不变量或 panic 才是。
规范 vs 实现
- API 与
go test集成:标准库/cmd/go文档保证的用法。 - 变异策略、覆盖率引导的具体算法、调度:属实现细节,随版本演进,不应被测试逻辑依赖。
边界情况与反直觉行为
1. 字节与字符串的 Unicode
按 string/[]byte 模糊时,要清楚函数语义是按字节还是按 rune。错误的 Reverse 按字节反转 UTF-8 会破坏往返——fuzz 很爱抓这个。
2. 无限循环
可能把机器跑挂。需要:
- 输入长度上限
- 复杂度护栏
-fuzztime
3. 非确定性
若目标用到全局随机/时间,先注入或在 fuzz 中固定,否则“失败”不可复现。
4. 语料库膨胀
长期 fuzz 可能堆积大量输入。定期审视 testdata/fuzz,保留有价值样本。
5. 多参数
f.Fuzz 可多参数;f.Add 参数类型与顺序必须匹配。
常见误区
[!warning] 常见误区:没有不变量,只“调用一下” 错误:fuzz 里只
Parse(in)不检查。
正确:至少保证不 panic,并在成功路径验证结构。
[!warning] 常见误区:CI 开超长 -fuzz 错误:每个 PR fuzz 10 分钟。
正确:CI 跑种子+corpus;探索放夜间/本地。
[!warning] 常见误区:把失败输入删了当解决 错误:删
testdata让测试绿。
正确:修代码,保留样本防回归。
[!warning] 常见误区:模糊替代表驱动 错误:不再写明确边界用例。
正确:表驱动固化规格;fuzz 挖未知。
工程实践
- 先表驱动后 fuzz:已知例子进表或
f.Add。 - 解析器/编解码优先接入 fuzz。
- 限制工作量:拷贝输入时注意长度;避免对巨型分配无上限。
- 与 race 组合谨慎:
go test -race -fuzz=...更慢,可阶段性用。 - 文档化不变量:在测试注释写清“维护什么性质”。
- Corpus 进版本库:让团队共享回归资产。
go test -fuzz=FuzzReverse -fuzztime=1m
go test -run=FuzzReverse # 仅 corpus/种子,不持续变异
可验证实验
实验 1:往返
对 Reverse 跑 30s fuzz,确认无失败。
实验 2:故意 mag 实现
按字节反转 UTF-8 字符串,看 fuzz 是否快速找到坏例并落盘。
实验 3:CI 模式
不传 -fuzz,确认仍执行 f.Add 种子。
实验 4:JSON round-trip
json.Marshal/Unmarshal 对结构体字段集合做不变量(注意 NaN 等边界,按类型选择)。
本节总结
- 本质:自动输入生成 + 不变量检查 + 失败语料沉淀。
- 工具链:与
go test统一;CI 回归 / 本地探索分工。 - 最易错:无不变量、删 corpus、把探索放进每次 PR。
- 下一步:对发现的样本补表驱动可读用例;热点再 bench。
自测题
概念题
- 不带
-fuzz时 fuzz 测试做什么? - 种子语料的作用?
- 什么样的断言适合放进
f.Fuzz?
代码推理题
f.Fuzz(func(t *testing.T, b []byte) {
_ = Parse(string(b))
})
弱点是什么?如何加强?
工程思考题
模糊 2 小时找到的 crasher 只在某同事机器上有。如何保证全员回归?
参考答案
展开
- 执行种子与已保存 corpus,不作持续变异。
- 引导早期覆盖有意义结构,加快发现。
- 不变量、往返、成功则合法、明确禁止 panic 的路径。
代码题:无断言,只能靠 panic;应检查 err 与成功后的结构/往返。
工程题:把失败输入提交到testdata/fuzz,CIgo test即可复现。
延伸阅读与资料来源
| 资料 | 类型 | 支撑 |
|---|---|---|
| Go Fuzzing | 官方文档 | 教程与概念 |
| testing — F | 标准库 | API |
| go test fuzz flags | 命令 | -fuzz/-fuzztime |
| Tutorial: Getting started with fuzzing | 教程 | 手把手 Reverse 例 |
笔记元信息
- 建议文件名:
go-fuzz-testing.md - 所属阶段:阶段五
- 学习顺序:表驱动之后
- 建议下一篇:竞态检测 或回到质量 MOC
- 本篇状态:已深化(书章结构;corpus 与 CI 分工)