【质量文化第四弹】经典推荐:Fuzzing Test

109 阅读3分钟
什么是Fuzzing Test

Fuzzing Test的核心理念是将自动或半自动生成的随机数据作为输入,以验证程序的健壮性。

这种测试方法特别适用于处理不可信输入源的场景,以及面向公共网络和具有安全要求的应用程序。 例如,针对一个以用户名作为输入的应用程序,为了验证其是否能正确处理无效数据,可以运用Fuzzing设计测试用例。这些测试用例会故意输入包含特殊字符或数字的数据,以验证这类异常输入是否会导致程序崩溃、内存泄漏或缓冲区溢出等问题。

Go从1.18版本开始在其标准工具链中支持Fuzzing Test,本文旨在通过简单例子带大家认识Fuzzing这项有意思的技术。

Fuzz使用方式

在这个案例里面我们使用变异类型进行Fuzzing Test,设计一个字符串翻转函数:

  • BadReverse1没有考虑多字节字符和无效字符串
  • BadReverse2没有考虑无效字符串
// 没有考虑多字节字符
func BadReverse1(s string) (string, error) {
    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), nil
}

// 没有考虑无效字符串
func BadReverse2(s string) (string, error) {
    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
}

// Good!
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
}

对应的单测和Fuzzing Test代码

// 普通测试无法发现问题
func TestReverse(t *testing.T) {
    testcases := []struct {
        in, want string
    }{
        {"Hello, world", "dlrow ,olleH"},
        {" ", " "},
        {"!12345", "54321!"},
    }
    for _, tc := range testcases {
        rev, err := Reverse(tc.in)
        if err != nil {
            return
        }
        if rev != tc.want {
                t.Errorf("Reverse: %q, want %q", rev, tc.want)
        }
    }
}

// 通过提供种子案例可以制造随机字符串
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)
        }
    })
}

我们将测试函数替换成 BadReverse 函数,会发现普通测试是通过的,但Fuzzing Test不通过。这是因为 BadReverse 函数没有考虑非法字符串和多字节字符的存在

而使用修正后的 Reverse 函数进行普通单测和Fuzzing Test十秒,均没问题

更多案例详情可查看go官方信息:go.dev/doc/tutoria…

结语

edge case平常不容易遇到,但大家都知道墨菲定律,任何错误不管概率多小都无法笃定它不会发生,而edge case一旦触达,结果可能会是灾难性的。 当然,Fuzzing Test不能替代单测,它通过大量的随机输入帮你找到edge case,找到后你需要加入到你的单测case里,让你的测试覆盖更完善,程序更加健壮。


以上是我在公司宣传的质量内建系列的第四期内容,背景是有同事曾遇到过edge case导致的灾难性事故,推荐Fuzzing Test技术,于是就有了这篇宣传,期望开发工程师能够编写健壮的代码,以及高场景覆盖的单测。

后续的每一期内容我都会及时发布在掘金的工程师质量文化专栏,欢迎大家关注,共同进步。 若需要原始海报资料,可以在文章下边评论留言,或是私信我获取。

最后,照例给大家看下我们在公司张贴的海报吧~

第四期-Fuzzing Test.jpeg