Go 1.18 系列篇(四):一文掌握 Fuzzing 模糊测试

2022年3月23日 369点热度 0人点赞 0条评论

系列导读


体验 Go 1.18(一):如何快速升级安装 Go 1.18 ?

体验 Go 1.18(二):一文掌握泛型用法

体验 Go 1.18(三):一文掌握 Go 工作区模式

# 1. 什么是模糊测试?

单元测试,大家应该都写过吧?单元测试,需要开发者根据函数逻辑,给定几组输入(入参)与输出(返回)的数据,然后 go test 根据这些数据集,调用函数,若返回值与预期相符,则说明函数的单元测试通过。

但单元测试的代码,也是由开发者写的一段一段代码,只要是代码,就会有 BUG,就会有遗漏的场景。

因此即使单元测试通过,也不代表你的程序没有问题。

可见,测试场景的数据集对于测试有多重要,而 Fuzzing 模糊测试就是一种用机器根据已知数据源,来自动生成测试数据的一种方案。

本文借用官方的一个例子来讲解。

# 2. 简单的示例

在开始之前,先初始化项目

go mod init github.com/iswbm/fuzz

然后在该项目中添加  main.go,内容如下

package main

import "fmt"

func Reverse(s string) string {
    b := [] byte(s)
    for i, j := 0len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
        b[i], b[j] = b[j], b[i]
    }
    return string(b)
}

func main() {
    input := "The quick brown fox jumped over the lazy dog"
    rev := Reverse(input)
    doubleRev := Reverse(rev)
    fmt.Printf("original: %q\n", input)
    fmt.Printf("reversed: %q\n", rev)
    fmt.Printf("reversed again: %q\n", doubleRev)
}

现在我们要为 Reverse 函数编写单元测试代码,放在 reverse_test.go,Test 函数如下

  • 给定了三组数据

  • 遍历这几组数据,将 tc.in 做为 Reverses 函数的入参执行函数,其返回值跟预期的 tc.want 做对比

  • 若不相等,则测试不通过~

package main

import (
    "testing"
)

func TestReverse(t *testing.T) {
    testcases := []struct {
        in, want string
    }{
        {"Hello, world""dlrow ,olleH"},
        {" "" "},
        {"!12345""54321!"},
    }
    for _, tc := range testcases {
        rev := Reverse(tc.in)
        if rev != tc.want {
                t.Errorf("Reverse: %q, want %q", rev, tc.want)
        }
    }
}

现在我们执行 go test 即是普通的单元测试,输出 PASS 说明单元测试通过,到目前为止是 Go 1.18 之前的单元测试

图片

然后我们再往 reverse_test.go 中加入 Fuzzing 模糊测试的代码

// 记得前面导入 "unicode/utf8" 包

func FuzzReverse(f *testing.F) {
    testcases := []string{"Hello, world"" ""!12345"}
    for _, tc := range testcases {
        f.Add(tc)  // Use f.Add to provide a seed corpus
    }
    f.Fuzz(func(t *testing.T, orig string) {
        rev := Reverse(orig)
        doubleRev := Reverse(rev)
        if orig != doubleRev {
            t.Errorf("Before: %q, after: %q", orig, doubleRev)
        }
        if utf8.ValidString(orig) && !utf8.ValidString(rev) {
            t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
        }
    })
}

Fuzzing 模糊测试的代码格式与单元测试很像:

  • 函数名固定以 Fuzz 开头(单元测试是以 Test 开头)

  • 函数固定以 *testing.F 类型做为入参(单元测试是以 *testing.T)

不一样的是 Fuzzing 模糊测试,提供两个函数:

  • t.Add:用于开发者输入模糊测试的种子数据,fuzzing 根据这些种子数据,自动随机生成更多测试数据

  • t.Fuzz:开始运行模糊测试,t.Fuzz 的入参是一个 Fuzz Target 函数(官方这么叫的),这个 Fuzz Target 函数的编写逻辑跟单元测试就一样了

在本例子中,Fuzz Target 接收 类型为 string 的入参,做为 Reverse 的输入源,然后利用两次 Reverse 的结果应与原字符串相等的原理进行测试。

有了 FuzzReverse 函数后,就可以使用如下命令进行模糊测试

go18 test -fuzz=Fuzz

通过输出发现测试并不顺利,Go 1.18 的 Fuzzing 会将导致测试异常的数据文件记录下来,使用 cat 可以查看该测试数据

图片

记录下来后,该数据就可做为普通单元测试的数据,此时我们再执行 go test 就会引用该数据,当然了,在问题解决之前, go test 会一直报错

图片

# 3. 问题排查与解决

模糊测试帮我们发现了一个出乎意料的 Bug 场景:在中文里的字符 其实是由3个字节组成的,如果按照字节反转,反转后得到的就是一个无效的字符串。

因此为了保证字符串反转后得到的仍然是一个有效的UTF-8编码的字符串,我们要按照rune进行字符串反转。

为了更好地方便大家理解中文里的字符 按照rune为维度有多少个rune,以及按照byte反转后得到的结果长什么样,我们对代码做一些修改。

图片

改完之后,再次执行 go test 就会提示测试成功,说明我们已经修复上面的那个场景的 BUG

图片

当下我们已经发现并修复了一个 BUG,程序肯定还有更多 BUG 存在,要继续寻找可以再次进行模糊测试,重复上面的步骤即可,这里不再赘述。

# 4. 更多参数介绍

在支持了 Fuzzing 模糊测试后,go test 工具也有了一些新的命令,在这里一并记录下

进行模糊测试

go test -fuzz=Fuzz

只对某个函数进行模糊测试:使用 -run=Fuzzxxx 或者 -fuzz=Fuzzxxx 指定模糊测试函数,避免执行到其他测试函数

go18 test -run=FuzzReverse
go18 test -fuzz=FuzzReverse

测试某个失败数据:使用 -run=file 指定数据文件

go test -run=FuzzReverse/1fdd0160e6b3dd8f1e6b7a4179b4787e0c014cf9c46c67a863d71e3a0277c213

指定模糊测试的时间:使用 -fuzztime 指定模糊测试时间或者迭代次数(默认无限期),避免一直在跑测试无法退出

还有一个 -fuzzminimizetime 参数,看官方文档的介绍,我没明白其作用,有知道的还请评论区分享下

go test -fuzz=Fuzz -fuzztime 30s

设置模糊测试进程数据:默认值是 $GOMAXPROCS,可根据实际情况进行设置,避免太占用机器的资源

go test -fuzz=Fuzz -parallel 4

# 5. 写在最后

模糊测试的存在,并不是为了替代原单元测试,而是为单元测试提供更好的保障,是一个补充方案,而非替代方案。

单元测试的局限性在于,你只能用预期的输入进行测试;模糊测试在发现暴露出奇怪行为的意外输入方面非常出色。一个好的模糊测试系统也会对被测试的代码进行分析,因此它可以有效地产生输入,从而扩大代码覆盖面。

同时模糊测试的适用场景也比较有限,如果函数的入参并不是像本例中的那样的简单(字符串),而是各种对象呢?可能它就无能为力了吧。

模糊测试的功能,对你有帮助吗?欢迎你留言分享~

   

图片

喜欢明哥文章的同学
欢迎点击卡片订阅!

⬇⬇⬇

73570Go 1.18 系列篇(四):一文掌握 Fuzzing 模糊测试

这个人很懒,什么都没留下

文章评论