Golang之模糊测试工具的使用

2023-03-06 15:59:26
目录
背景开发环境go-fuzzingfuzz tests规则如何使用go-fuzzing生产环境项目Go版本问题

背景

我们经常调侃程序员每天都在写bug,这确实是事实,没有测出bug不代表程序就真的不存在问题。传统的代码review、静态分析、人工测试和自动化的单元测试无法穷尽所有输入组合,尤其是难以模拟一些随机的、边缘的数据。

去年6月,Go官方发布称gotip版本已经原生支持Fuzzing并开始了公测,将与[Go>

说到[Go 1.18版本],大家最关注的应该是泛型,但是我个人觉得go-fuzzing也是其中的一个亮点,Go 1.18将fuzz testing纳入了go test工具链,与单元测试、性能基准测试等一起成为了Go原生测试工具链中的重要成员。

本次就来说下go-fuzzing这个工具。

开发环境

升级到Go>Go 1.18虽然还没正式发布,但可以下载RC版本,而且即使你生产环境用是Go的老版本,你个人的本地开发环境也可以升级到1.18,还可以使用go-fuzzing更好的自测

go-fuzzing

官方文档:go>

fuzzing,又叫fuzz testing,中文叫做模糊测试或随机测试。其本质上是一种自动化测试技术,更具体一点,它是一种基于随机输入的自动化测试技术,常被用于发现处理用户输入的代码中存在的bug和问题。

fuzz>
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, int64uint, uint8/byte, uint16, uint32, uint64string, []bytefloat32, float64bool

      如何使用go-fuzzing

      1、首先要先定义fuzzing>

      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

      到此这篇关于Golang之模糊测试工具的使用的文章就介绍到这了,更多相关Golang 模糊测试 内容请搜索易采站长站以前的文章或继续浏览下面的相关文章希望大家以后多多支持易采站长站!