Go fuzzing模糊测试

Go fuzzing模糊测试

fuzzing模糊测试在Go1.18中引入。官方文档:Tutorial: Getting started with fuzzing - The Go Programming Language,本文主要以官方文档的fuzzing入门教程为主,精简自认为重要的内容与自己的理解。因此相比官方文档略有简略,有步骤省略请查阅官方文档或参考资料中的翻译版本。

编写待测函数(写一个BUG)

main.go文件中写入函数内容:

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)
}

该函数功能较简单,可以翻转字符串。在main.go中调用这个函数试试!ps:如上所述,内容较精简,只包含核心代码,如包声明,模块导入代码就省略了

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)
}

运行结果为

$ go run .
original: "The quick brown fox jumped over the lazy dog"
reversed: "god yzal eht revo depmuj xof nworb kciuq ehT"
reversed again: "The quick brown fox jumped over the lazy dog"

可以看到使用字符串"The quick brown fox jumped over the lazy dog"作为函数的参数,函数正常执行且达到了预期效果。

单元测试(unit test)

创建reverse_test.go文件编写单元测试函数。

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运行单元测试

$ go test
PASS 
ok      example/fuzz  0.013s

测试顺利通过。接下来将单元测试改成模糊测试(fuzzing)

模糊测试(fuzz test)

The unit test has limitations, namely that each input must be added to the test by the developer. One benefit of fuzzing is that it comes up with inputs for your code, and may identify edge cases that the test cases you came up with didn’t reach.

单元测试有局限性,每个测试输入必须由开发者指定加到单元测试的测试用例里。fuzzing的优点之一是可以基于开发者代码里指定的测试输入作为基础数据,进一步自动生成新的随机测试数据,用来发现指定测试输入没有覆盖到的边界情况。

注意:你可以把单元测试、性能测试(benchmarks)和模糊测试放在同一个*_test.go文件里。

编写模糊测试

reverse_test.go里的单元测试代码TestReverse替换成如下的模糊测试代码FuzzReverse

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)
        }
    })
}
  • 模糊测试也有一定的局限性。由于模糊测试的原理是将测试输入加入语料库(corpus)后自动生成新的随机测试数据,因此我们无法预期输出结果。相反的在单元测试中预期输出结果是可知的。

  • 由于预期结果未知,在该测试函数中我们只能对Reverse的两个特性进行验证

    1. 对一个字符串做2次反转,得到的结果和源字符串相同

    2. 反转后的字符串也仍然是一个有效的UTF-8编码的字符串

注意:fuzzing模糊测试和Go已有的单元测试以及性能测试框架是互为补充的,并不是替代关系。

Go模糊测试和单元测试在语法上有如下差异:

  • Go模糊测试函数以FuzzXxx开头,单元测试函数以TestXxx开头
  • Go模糊测试函数以 *testing.F作为入参,单元测试函数以*testing.T作为入参
  • Go模糊测试会调用f.Add函数和f.Fuzz函数。
    • f.Add函数把指定输入作为模糊测试的种子语料库(seed corpus),fuzzing基于种子语料库生成随机输入。
    • f.Fuzz函数接收一个fuzz target函数作为入参。fuzz target函数有多个参数,第一个参数是*testing.T,其它参数是被模糊的类型(注意:被模糊的类型目前只支持部分内置类型, 列在 Go Fuzzing docs,未来会支持更多的内置类型)。

Fuzz函数分析

运行模糊测试
  1. 执行如下命令来运行模糊测试。

    这个方式只会使用种子语料库,而不会生成随机测试数据。通过这种方式可以用来验证种子语料库的测试数据是否可以测试通过。(fuzz test without fuzzing)

    $ go test
    PASS
    ok      example/fuzz  0.013s
    

    如果reverse_test.go文件里有其它单元测试函数或者模糊测试函数,但是只想运行FuzzReverse模糊测试函数,我们可以执行go test -run=FuzzReverse命令。

    注意go test默认会执行所有以TestXxx开头的单元测试函数和以FuzzXxx开头的模糊测试函数,默认不运行以BenchmarkXxx开头的性能测试函数,如果我们想运行 benchmark用例,则需要加上 -bench 参数。

  2. 如果要基于种子语料库生成随机测试数据用于模糊测试,需要给go test命令增加 -fuzz参数。(fuzz test with fuzzing)

    $ go test -fuzz=Fuzz
    fuzz: elapsed: 0s, gathering baseline coverage: 0/3 completed
    fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed, now fuzzing with 8 workers
    fuzz: minimizing 38-byte failing input file...
    --- FAIL: FuzzReverse (0.01s)
        --- FAIL: FuzzReverse (0.00s)
            reverse_test.go:20: Reverse produced invalid UTF-8 string "\x9c\xdd"
    
        Failing input written to testdata/fuzz/FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a
        To re-run:
        go test -run=FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a
    FAIL
    exit status 1
    FAIL    example/fuzz  0.030s
    

    上面的fuzzing测试结果是FAIL,引起FAIL的输入数据被写到了一个语料库文件里。下次运行go test命令的时候,即使没有-fuzz参数,这个语料库文件里的测试数据也会被用到。

    可以用文本编辑器打开testdata/fuzz/FuzzReverse目录下的文件,看看引起Fuzzing测试失败的测试数据长什么样。下面是一个示例文件,你那边运行后得到的测试数据可能和这个不一样,但文件里的内容格式会是一样的。

    go test fuzz v1
    string("泃")
    

    语料库文件里的第1行标识的是编码版本(encoding version,说直白点,就是这个种子语料库文件里内容格式的版本),虽然目前只有v1这1个版本,但是Fuzzing设计者考虑到未来可能引入新的编码版本,于是加了编码版本的概念。

    从第2行开始,每一行数据对应的是语料库的每条测试数据(corpus entry)的其中一个参数,按照参数先后顺序排列。

    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 %q", orig, rev)
            }
    })
    

    本文的FuzzReverse里的fuzz target函数func(t *testing.T, orig string)只有orig这1个参数作为真正的测试输入,也就是每条测试数据其实就1个输入,因此在上面示例的testdata/fuzz/FuzzReverse目录下的文件里只有string("泃")这一行。

    如果每条测试数据有N个参数,那fuzzing找出的导致fuzz test失败的每条测试数据在testdata目录下的文件里会有N行,第i行对应第i个参数。

  3. 再次运行go test命令,这次不带-fuzz参数。

    我们会发现虽然没有-fuzz参数,但是模糊测试的时候仍然用到了上面第2步找到的测试数据。

    $ go test
    --- FAIL: FuzzReverse (0.00s)
        --- FAIL: FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a (0.00s)
            reverse_test.go:20: Reverse produced invalid string
    FAIL
    exit status 1
    FAIL    example/fuzz  0.016s
    

    既然Go fuzzing测试没通过,那就需要我们调试代码来找出问题所在了。

修BUG并反复运行模糊测试

该部分不是模糊测试的核心内容且内容较多。主要修了两个BUG,主要和[]byte[]rune相关。具体内容详见:

补充一点:fuzz test如果没有遇到错误,默认会一直运行下去,需要使用 ctrl-C 结束测试。

也可以传递-fuzztime参数来指定测试时间,这样就不用 ctrl-C 了。

总结

重点

  • 单元测试和模糊测试的区别:
    • 单元测试可知预期结果;单元测试测试输入由测试者提供,数量有限
    • 模糊测试输入未知因此预期结果不可知;模糊测试中测试者只需将有限的测试输入使用t.Add加入种子语料库,在模糊测试中即可自动生成若干测试输入
    • 由上述差别导致单元测试和模糊测试的测试逻辑略有不同。
  • go test的使用
    • go test只会使用种子语料库中的测试输入和以往测试中失败存放在文件中的测试输入进行测试。
    • go test -fuzz=Fuzz则会基于种子语料库生成若干随机测试输入进行模糊测试。
    • 如在模糊测试中未遇到错误默认无限运行,需使用CTRL+C手动结束测试。当然,也可以在使用go test时加入参数-fuzztime参数指定测试时间。

References

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值