go 1.18 新特性:『模糊测试』

2022年3月27日 342点热度 0人点赞 0条评论

大家好,我是 阿星,今天给大家分享一篇 go 1.18新特性的另外一个特性,『模糊测试』。这个概念,我也是第一次听说,因而激起了我的一些兴趣,翻阅了一些资料,结合 go 1.18 的语法,我们一起来看看这个新知识吧。

序0:模糊测试是什么

Fuzz Testing或Fuzzing是一种软件测试技术,将称为FUZZ的无效或随机数据放入软件系统中,以发现编码错误和安全漏洞。模糊测试的目的是使用自动化或半自动化技术插入数据,并对系统进行各种异常测试,例如系统崩溃或内置代码故障等。


简单来说就是提供一些无效数据和预料之外的数据来对我们的编码进行测试,属于安全测试范围内的一种测试手段,能够帮助我们测试出编码中更多的问题。


作为一个开发人员,基本都知道单元测试是保证我们程序稳定运行的一个保障,因而很多公司的代码提交平台也需要单元测试的代码覆盖率达到多少以上才允许合并,但单元测试能过,也不能代表我们程序不会出现bug。因为按照我们的编码思路来写自己的单元测试,经常会囿于自己的编程思维之中,都很容易漏掉一些正向思维难以步骤到的异常场景。


「不识庐山真面目,只缘身在此山中」,因而,我们需要「模糊测试」来对我们的程序提供一个更可靠的保障。


序1:模糊测试的工作原理

模糊测试的工作原理:

图片

  1. 输入模糊测试的语料库并生成模糊数据

  2. 执行系统单元测试并监控系统行为

  3. 记录bug

工作原理很简单,也就是一些简单的官方解释,没什么需要着重介绍的,下面直接来看看 go 语言原生支持的模糊测试具体应该怎么使用。

序2:在 go 语言中如何使用模糊测试

go 的模糊测试提供了两个方法:


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

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

下面来看个示例,我们有这样一个反转字符串的例子:


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

对其编写单元测试:

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)        }    }}
测试结果:
=== RUN   TestReverse--- PASS: TestReverse (0.00s)PASS
对其编写模糊测试:
func FuzzReverse(f *testing.F) {  testcases := []string{"Hello, world", " ", "!12345"}  for _, tc := range testcases {    f.Add(tc)  // 1. 用前面的测试case作为输入预料库  }  //2. f.Fuzz() 就会随机生成一些模糊测试数据并执行单元测试  f.Fuzz(func(t *testing.T, orig string) {    rev := Reverse(orig)    doubleRev := Reverse(rev)    //3. 处理结果    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)    }  })}
测试结果:
fuzz: elapsed: 0s, gathering baseline coverage: 0/8 completedfailure while testing seed corpus entry: FuzzReverse/c05903cd28106c02aebdaccb2f39e8f55f62c75cbac4d0bd82f1ae0fe19d1425fuzz: elapsed: 0s, gathering baseline coverage: 3/8 completed--- FAIL: FuzzReverse (0.02s)    --- FAIL: FuzzReverse (0.00s)        main_test.go:25: Reverse produced invalid UTF-8 string "\xb4\xce000000"
FAILexit status 1FAIL    utils/retry     0.026s
嗯?竟然测试不通过。很显然,模糊测试发现了单元测试中没有发现的漏洞,比如本例中没有考虑到字符串的编码规则,正常开发过程中,其实也很容易忽视这些细节。但是引入模糊模糊测试就能很好地避免这一点。
修正后的代码为:
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 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, err1 := Reverse(orig)    if err1 != nil {      return    }    doubleRev, err2 := Reverse(rev)    if err2 != nil {      return    }    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)    }  })}
再次执行模糊测试结果:
fuzz: elapsed: 0s, gathering baseline coverage: 0/8 completedfuzz: elapsed: 0s, gathering baseline coverage: 8/8 completed, now fuzzing with 8 workersfuzz: elapsed: 3s, execs: 290059 (96583/sec), new interesting: 32 (total: 40)fuzz: elapsed: 6s, execs: 843477 (184663/sec), new interesting: 35 (total: 43)fuzz: elapsed: 9s, execs: 1255007 (137157/sec), new interesting: 37 (total: 45)fuzz: elapsed: 12s, execs: 1676071 (140285/sec), new interesting: 37 (total: 45)fuzz: elapsed: 15s, execs: 2091844 (138668/sec), new interesting: 37 (total: 45)fuzz: elapsed: 18s, execs: 2513711 (140623/sec), new interesting: 37 (total: 45)fuzz: elapsed: 21s, execs: 2919301 (135200/sec), new interesting: 37 (total: 45)fuzz: elapsed: 24s, execs: 3318661 (133099/sec), new interesting: 37 (total: 45)fuzz: elapsed: 27s, execs: 3732331 (137872/sec), new interesting: 37 (total: 45)fuzz: elapsed: 30s, execs: 4150770 (139518/sec), new interesting: 37 (total: 45)^Cfuzz: elapsed: 30s, execs: 4177381 (102922/sec), new interesting: 37 (total: 45)PASSok      utils/retry     30.277s
跑了一段时间,通过!
模糊测试这种基于输入语料,随机生成测试数据的方式,能够避免开发基于正向编码思维编写单元测试而遗漏的场景,能够保证程序更加稳定地运行,毕竟用户的输入更是我们无法预料到的。

序4:总结

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

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


参考资料:

https://go.dev/doc/tutorial/fuzz

今天的分享就到这里了,如果本文对您有帮助,还是希望多多点赞,转发,分享,谢谢您的支持~


更多内容,可点击下方关注公众号~


73250go 1.18 新特性:『模糊测试』

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

文章评论