Fuzzing で Go の関数に想定外の入力が渡された際のバグを見つける
golangFuzzing はランダムなデータを入力とするテスト手法で、Go では 1.18 からサポートされている。
次の FizzBuzz 関数をテストする。
func FizzBuzz(n uint64) string {
if n%3 == 0 {
if n%5 == 0 {
return "FizzBuzz"
}
return "Fizz"
}
if n%5 == 0 {
return "Buzz"
}
return strconv.Itoa(int(n))
}
まずはテーブルドリブンテストを行う。
func TestFizzBuzz(t *testing.T) {
testcases := []struct {
title string
in uint64
out string
}{
{"A multiple of 3 returns Fizz", 3, "Fizz"},
{"A multiple of 5 returns Buzz", 5, "Buzz"},
{"A multiple of 3 and 5 returns FizzBuzz", 15, "FizzBuzz"},
{"Otherwise returns number string", 1, "1"},
}
for _, testcase := range testcases {
t.Run(testcase.title, func(t *testing.T) {
ret := FizzBuzz(testcase.in)
if ret != testcase.out {
t.Errorf("FizzBuzz(%d) = %s, want %s", testcase.in, ret, testcase.out)
}
})
}
}
実行すると全てのテストケースを通過するので問題ないと言いたいところだが、実はこの関数にはバグがある。
$ go test
PASS
ok github.com/sambaiz/gofuzztest 0.093s
そもそも想定外の入力に由来するバグの場合テストケースからも漏れてしまうことが多く、また、入力のパターンが多いと網羅し切れないこともある。 そのような場合、Fuzzing がバグ発見の助けになるかもしれない。なお今のところ map や struct といった型には対応していない。
func FuzzFizzBuzz(f *testing.F) {
// f.Add(9223372036854775807)
f.Fuzz(func(t *testing.T, n uint64) {
ret := FizzBuzz(n)
if ret != strconv.FormatUint(n, 10) &&
ret != "Fizz" &&
ret != "Buzz" &&
ret != "FizzBuzz" {
t.Errorf("FizzBuzz(%d) = %s, want %s, Fizz, Buzz, or FizzBuzz",
n, ret, strconv.FormatUint(n, 10))
}
})
}
実行するとカバレッジを上げる interesting な入力を探索し始める。結果、入力がとても大きいときに期待しない値が返ることがわかった。
$ go test -fuzz=FuzzFizzBuzz -fuzztime 10s
fuzz: elapsed: 0s, gathering baseline coverage: 0/8 completed
fuzz: elapsed: 0s, gathering baseline coverage: 8/8 completed, now fuzzing with 8 workers
fuzz: elapsed: 0s, execs: 16 (143/sec), new interesting: 0 (total: 8)
--- FAIL: FuzzFizzBuzz (0.11s)
--- FAIL: FuzzFizzBuzz (0.00s)
main_test.go:39: FizzBuzz(9223372036854775873) = -9223372036854775743, want 9223372036854775873, Fizz, Buzz, or FizzBuzz
Failing input written to testdata/fuzz/FuzzFizzBuzz/b0764ff8575cbc59
To re-run:
go test -run=FuzzFizzBuzz/b0764ff8575cbc59
FAIL
exit status 1
FAIL github.com/sambaiz/gofuzztest 0.323s
これは uint64 を int(64) に変換しているのが原因だ。修正した後、失敗した入力の corpus ファイルで再度テストすることができる。
$ cat testdata/fuzz/FuzzFizzBuzz/b0764ff8575cbc59
go test fuzz v1
uint64(9223372036854775873)
$ go test -run=FuzzFizzBuzz/b0764ff8575cbc59
この corpus ファイルや f.Add() によって seed を与えることで baseline coverage が上がり効率的な探索が行われるようになるようだ。