Golang之模糊测试工具的使用
作者:sheliutao 发布时间:2023-07-01 19:47:47
背景
我们经常调侃程序员每天都在写bug,这确实是事实,没有测出bug不代表程序就真的不存在问题。传统的代码review、静态分析、人工测试和自动化的单元测试无法穷尽所有输入组合,尤其是难以模拟一些随机的、边缘的数据。
去年6月,Go官方发布称gotip版本已经原生支持Fuzzing并开始了公测,将与[Go 1.18版本]一起在2022年中发布,go-fuzzing至今已经发现了Go标准库超过200个bug(https://github.com/dvyukov/go-fuzz#trophies )。即将发布的[Go 1.18版本]就提供了一个代码自测的绝佳工具go-fuzzing。
说到[Go 1.18版本],大家最关注的应该是泛型,但是我个人觉得go-fuzzing也是其中的一个亮点,Go 1.18将fuzz testing纳入了go test工具链,与单元测试、性能基准测试等一起成为了Go原生测试工具链中的重要成员。
本次就来说下go-fuzzing这个工具。
开发环境
升级到Go 1.18
Go 1.18虽然还没正式发布,但可以下载RC版本,而且即使你生产环境用是Go的老版本,你个人的本地开发环境也可以升级到1.18,还可以使用go-fuzzing更好的自测
go-fuzzing
官方文档:go fuzzing是通过持续给一个程序不同的输入来自动化测试,并通过分析代码覆盖率来智能的寻找失败的例子。这种方法可以尽可能的找到一些边界问题,亲测确实发现的都是些平时比较难发现的问题。
fuzzing,又叫fuzz testing,中文叫做模糊测试或随机测试。其本质上是一种自动化测试技术,更具体一点,它是一种基于随机输入的自动化测试技术,常被用于发现处理用户输入的代码中存在的bug和问题。
fuzz tests规则
func FuzzFoo(f *testing.F) {
f.Add(5, "hello")
f.Fuzz(func(t *testing.T, i int, s string) {
out, err := Foo(i, s)
if err != nil && out != "" {
t.Errorf("%q, %v", out, err)
}
})
}
函数必须是Fuzz开头,唯一的参数只有*testing.F,没有返回值
Fuzz tests必须在名为*_test.go的文件下才能执行
fuzz target是个方法,它调用(*testing.F).Fuzz,第一个参数是 *testing.T,之后的参数就是称之为fuzzing arguments的参数,方法没有返回值
每个fuzz test中只能有一个fuzz target
调用f.Add()的时候需要参数类型跟fuzzing arguments顺序和类型都保持一致
fuzzing arguments只支持以下类型:
int, int8, int16, int32/rune, int64
uint, uint8/byte, uint16, uint32, uint64
string, []byte
float32, float64
bool
如何使用go-fuzzing
1、首先要先定义fuzzing arguments,并通过fuzzing arguments写fuzzing target
2、思考fuzzing target怎么写,重点是怎么验证结果的正确性,因为fuzzing arguments是随机给的,所以要有个验证结果的方法
3、遇到失败的例子怎么去打印出错误结果
4、根据错误结果去生成新的测试用例,这个新的测试用例会被用来调试发现的bug,并且可以留下给CI使用
下面是一个切片中数字求和的例子:
// slice_sum.go
func SliceSum(arr []int64) int64 {
var sum int64
for _, val := range arr {
if val % 100000 != 0 {
sum += val
}
}
return sum
}
第一步:定义fuzzing arguments模糊参数
至少需要给出一个fuzzing arguments,不然go-fuzzing没法生成测试代码。
这是切片中元素求和的方法,那我们可以把切片的元素个数n(自行模拟个数即可)作为fuzzing arguments,然后go-fuzzing会根据运行的代码覆盖率自动生成不同的参数来模拟测试。
// slice_sum_test.go
func FuzzSliceSum(f *testing.F) {
// 10,go-fuzzing称之为语料,10这个值就是让go fuzzing冷启动的一个值,具体多少不重要
f.Add(10)
f.Fuzz(func(t *testing.T, n int) {
// 限制20个元素
n %= 20
// 剩余处理
})
}
第二步:编写fuzzing target
重点是编写可以验证的fuzzing target,不仅要根据给定的模糊参数写出测试代码,而且还需要生成可以验证结果正确性的数据。
对这个切片元素求和的方法来说,就是随机生成n个元素的切片,然后进行求和得到正确的结果。
package fuzz
import (
"github.com/stretchr/testify/assert"
"math/rand"
"testing"
"time"
)
// slice_sum_test.go
func FuzzSliceSum(f *testing.F) {
// 初始化随机数种子
rand.Seed(time.Now().UnixNano())
// 语料
f.Add(10)
f.Fuzz(func(t *testing.T, n int) {
n %= 20
var arr []int64
var expect int64 // 期望值
for i := 0; i < n; i++ {
val := rand.Int63() % 1000000
arr = append(arr, val)
expect += val
}
// 自己求和的结果和调用函数求和的结果比对
assert.Equal(t, expect, SliceSum(arr))
})
}
执行模糊测试
➜ fuzz go test -fuzz=SliceSum
fuzz: elapsed: 0s, gathering baseline coverage: 0/52 completed
fuzz: elapsed: 0s, gathering baseline coverage: 52/52 completed, now fuzzing with 8 workers
fuzz: elapsed: 0s, execs: 9438 (34179/sec), new interesting: 2 (total: 54)
--- FAIL: FuzzSliceSum (0.28s)
--- FAIL: FuzzSliceSum (0.00s)
slice_sum_test.go:32:
Error Trace: slice_sum_test.go:32
value.go:556
value.go:339
fuzz.go:337
Error: Not equal:
expected: 5715923
actual : 5315923
Test: FuzzSliceSum
Failing input written to testdata/fuzz/FuzzSliceSum/8e8981ffa4ee4d93f475c807563f9d63854a6c913cdfb10a73191549318a2a51
To re-run:
go test -run=FuzzSliceSum/8e8981ffa4ee4d93f475c807563f9d63854a6c913cdfb10a73191549318a2a51
FAIL
exit status 1
FAIL demo/fuzz 0.287s
上面这段输出,你只能看出预期值和实际值不一样,但是很难分析错误。
第三步:打印出错误的例子
上面的错误输出,如果能打印出造成错误的例子的话,就可以直接作为测试用例进行单测。我们总不能一个个去试吧,而且错误的例子未必只有一个。
修改下模糊测试代码,增加打印:
package fuzz
import (
"github.com/stretchr/testify/assert"
"math/rand"
"testing"
"time"
)
// slice_sum_test.go
func FuzzSliceSum(f *testing.F) {
// 初始化随机数种子
rand.Seed(time.Now().UnixNano())
// 语料
f.Add(10)
f.Fuzz(func(t *testing.T, n int) {
n %= 20
var arr []int64
var expect int64 // 期望值
var buf strings.Builder
buf.WriteString("\n")
for i := 0; i < n; i++ {
val := rand.Int63() % 1000000
arr = append(arr, val)
expect += val
buf.WriteString(fmt.Sprintf("%d,\n", val))
}
// 自己求和的结果和调用函数求和的结果比对
assert.Equal(t, expect, SliceSum(arr), buf.String())
})
}
再次执行模糊测试
➜ fuzz go test -fuzz=SliceSum
fuzz: elapsed: 0s, gathering baseline coverage: 0/47 completed
fuzz: elapsed: 0s, gathering baseline coverage: 47/47 completed, now fuzzing with 8 workers
fuzz: elapsed: 0s, execs: 17109 (42507/sec), new interesting: 2 (total: 49)
--- FAIL: FuzzSliceSum (0.41s)
--- FAIL: FuzzSliceSum (0.00s)
slice_sum_test.go:34:
Error Trace: slice_sum_test.go:34
value.go:556
value.go:339
fuzz.go:337
Error: Not equal:
expected: 7575516
actual : 7175516
Test: FuzzSliceSum
Messages:
92016,
642504,
400000,
489403,
472011,
811028,
315130,
298207,
57765,
542614,
136594,
351360,
867104,
918715,
515092,
665973,
Failing input written to testdata/fuzz/FuzzSliceSum/9191ba4d7ea5420a9a76661d4e7d6a7a4e69ad4d5d8ef306ff78161a2acf1416
To re-run:
go test -run=FuzzSliceSum/9191ba4d7ea5420a9a76661d4e7d6a7a4e69ad4d5d8ef306ff78161a2acf1416
FAIL
exit status 1
FAIL demo/fuzz 0.413s
第四步:根据输出的错误例子,编写新的测试用例进行单测
// 单测通过后,再执行模糊测试,看看有没有其他边缘问题出现
func TestSliceSumFuzzCase1(t *testing.T) {
arr := []int64{
92016,
642504,
400000,
489403,
472011,
811028,
315130,
298207,
57765,
542614,
136594,
351360,
867104,
918715,
515092,
665973,
}
// 期望值从第三步的输出中获取
assert.Equal(t, int64(7575516), SliceSum(arr))
}
这样就可以很方便的进行调试了,并且能够增加有效的测试用例进行单测,确保这个bug不会出现了。
生产环境项目Go版本问题
线上项目的Go版本不能升级到1.18怎么办?
线上的版本不升级到1.18,但是我们本地开发升级没有问题,可以在文件的头部增加如下命令注释:
slice_sum_test.go
//go:build go1.18
// +build go1.18
这样我们在线上不管用哪个版本都不会报错,而且我们一般都是在本地进行模糊测试
注意:第三行必须是空行,不然就会变成package的注释了
有些还无法复现的问题,比如协程死锁,输出一直在执行或者卡住然后过一会才结束,这类的长时间执行的模糊测试,我还没有摸透。如果有大佬知道的话麻烦也告诉我下。
参考
https://github.com/dvyukov/go-fuzz#trophies
https://go.dev/blog/fuzz-beta
来源:https://blog.csdn.net/weixin_42526674/article/details/129329223
猜你喜欢
- 举例: 如:在字段名处输入:username,password,email,telphone 注意:不同的字段名用英文逗号隔开,且不支持星号
- DEMO:var testobj = document.getElementById("test&q
- 就是在mysql命令行登录的时候加上: --pager=more 参数可以使用linux下的more来分页,很好用
- 很多时候,由于程序设计需要,要求在asp的include包含文件里调用动态的文件。如<!--#include file=&q
- 以图像处理见长的微软Live实验室,最近发布了一款新作:Pivot。装完启动后的第一印象就是一款浏览器,和IE、FF、Chrome又不太一样
- asp ajax json教程首先用ACCESS建json.mdb的库,然后建一个表t_jsontable,字段如下:jt_id,jt_na
- 下面是一份在 HTML 4 Strict 和 XHTML 1.0 Strict 下必须遵守的标签嵌套规则,比如你不能在 <a>
- 前提条件:1.安装好Wampserver64(版本不限)2.Wampserver64软件启动后 变为绿色如:3.在数据库里面创建好名为&am
- 下面我们以论坛排行榜举例说明:<% @ LANGUAGE="VBSCRIPT" %&
- 常见的双倍边距类问题都遇到过,但很少遇到这种有意思的,所以记录一下。这个BUG是发生在Standards模式下(就是包含XHTML或者HTM
- 产品使用者可分为三个类别,分别是:初级用户、中级用户、高级用户。这三个类别分别代表了用户使用产品的三个阶段,其中初级用户是占最大比例的,中级
- 与部门同事做了个小小的交流,话题杂而浅,在此做一个小纪录。1、什么是设计工业设计、环境设计、建筑设计、平面设计、网页设计、服装设计、信息设计
- 相信为数不少的系统管理员每天都在做着同一样的工作——对数据进行备份。一旦哪一天疏忽了,而这一天系统又恰恰发生了故障,需要进行数据
- 【原文地址】New C# "Orcas" Language Features: Automatic Properties
- 快照复制是在数据库之间对数据以及数据库对象进行复制并进行同步,以确保多个数据库之间一致性的一个法宝。简单的说,快照复制就是实现把一个数据库服
- 看到这个需求的时候就在暗爽,又可以搞定一个知识点了。哈哈,一天的奋斗之后,果然有所收获,而且经过怿飞的指点,在跨域问题解决上还有所突破(不通
- 可以查看: 代码如下:OPEN SYMMETRIC KEY 命令关于 对称密钥加密使用证书解密 CREATE MASTER KEY ENC
- 有效地加载数据有时我们需大量地把数据加载到数据表,采用批量加载的方式比一个一个记录加载效率高,因为MySQL不用每加载一条记录就刷新一次索引
- form无论是在网站的制作中,还是在网站的重构中,我们都会频繁地“碰面”,当“碰面”的次数多了,反而觉得他更让人迷茫,有种熟悉的“陌生”,越
- 今天在一个QQ群中看到有人在问一个进度条的实现方式,当时因为工作时间,需求相对也比较紧,只是简单的说了一下可以通过CSS的边框属性和背景属性