🙆
GoのFuzzingテストやってみる
最近出たこちらの本を読んでいたら、Fuzzingテストという機能を知ったので、やってみました
以下のチュートリアルを見ながら触ってみます
実際に触ってみたリポジトリはこちら
通常のユニットテスト
チュートリアルでは、文字列を逆転させて吐き出す関数を例に出しています
main.go
package main
import "fmt"
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)
}
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)
}
実行してみると、アルファベットでは問題なく動いているようです。
$ 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"
ユニットテストを書いてみます。
main_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 -run TestReverse
PASS
ok 10inoino/fizzing-test-demo 0.106s
Fuzzingテストを実装
しかし、文字列はアルファベットだけではありません。
本来様々な文字列で検証しなければいけませんが、他の文字列のパターンを調べてテストケースとして追加するのも難しい。
そんなときにはFuzzingテストを使ってみましょう。
Fuzzingテストを実装します。
main_test.go
package main
import (
"testing"
"unicode/utf8"
)
func TestReverse(t *testing.T) {
// ・・・
}
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)
}
})
}
Fuzzingテストのルールは以下のとおりです。
(https://go.dev/security/fuzz/ より引用)
- ファズテストはFuzzXxxのような名前の関数でなければならず、*testing.Fだけを受け取り、戻り値はない。
- Fuzzテストは、*_test.goファイルでないと実行できません。
- ファズターゲットは、最初のパラメータとして*testing.Tを受け入れ、ファジング引数が続く(*testing.F).Fuzzへのメソッド呼び出しでなければなりません。戻り値はありません。
- ファズテストにつき、ファズターゲットは正確に1つでなければならない。
- これは、(*testing.F).Addの呼び出しと、ファズテストのtestdata/fuzzディレクトリ内のすべてのコーパスファイルに対して当てはまります。
- ファジングの引数は、以下の型のみです。
- string,[]byte
- int,int8,int16,int32/rune,int64
- uint、uint8/ byte、uint16、uint32、uint64
- float32、float64
- bool
Fuzzingテストを実行してみます。
-fuzz=
のあとに、正規表現でテストする関数が指定できるようです。
$ go test -fuzz=Fuzz
fuzz: elapsed: 0s, gathering baseline coverage: 0/14 completed
failure while testing seed corpus entry: FuzzReverse/a7e3a6205b275a47
fuzz: elapsed: 0s, gathering baseline coverage: 5/14 completed
--- FAIL: FuzzReverse (0.03s)
--- FAIL: FuzzReverse (0.00s)
main_test.go:36: Reverse produced invalid UTF-8 string "\x91\xdd"
FAIL
exit status 1
FAIL 10inoino/fizzing-test-demo 0.303s
テストが失敗しました。
エラーメッセージに失敗した文言を出力することもできますが、testdata配下に失敗したテストデータが格納されているので、こちらを参照しても良いでしょう。
testdata/fuzz/FuzzReverse/a7e3a6205b275a47
go test fuzz v1
string("ݑ")
今回は、ݑでテストが実行されていたようです。
試しに、FuzzReverseディレクトリごと削除して、再度Fuzzingテストを実行すると、他のテストデータでもテストが失敗することがわかります。
testdata/fuzz/FuzzReverse/d8b27a9830e81c8b
go test fuzz v1
string("\ue104")
1つ注意点として、Fuzzingテストはテストが失敗するまで実行し続けるので、タイムアウトの時間を設定して実行するのが良いでしょう。
go test -fuzz=Fuzz -fuzztime 10s
バリデーションの確認などで、とにかく様々な種類の引数をテストしてみたいという場合に、すごく便利ですね👏
Discussion