Go言語入門:Fuzzing -Vol.2-

スポンサーリンク
Go言語入門:Fuzzing -Vol.2- 用語解説
Go言語入門:Fuzzing -Vol.2-
この記事は約7分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語のFuzzingついて解説しています。

スポンサーリンク

背景

Goでテストを書いていると、「Fuzzing」という言葉を目にすることがあるかもしれません。公式ドキュメントを開いてみたものの、英語で書かれていて「coverage guidance」「edge cases」といった専門用語が並び、正直なところ「何をするものなのか、よくわからない…」と感じた方も多いのではないでしょうか。

実は私も最初は同じでした。「自動テスト」「入力を操作」「セキュリティ脆弱性の発見」と聞いても、具体的にどう使えばいいのか、なぜ必要なのかがピンと来なかったんです。

そこでこの記事では、Go公式ドキュメントの「Fuzzing」について、丁寧な日本語訳はもちろん、初心者の方にもわかるように補足説明や具体例を交えながら解説していきます。Fuzzingは一見難しそうに見えますが、実は「人間が思いつかないようなテストケースを自動で試してくれる便利な仕組み」なんです。

テストコードをより強固にしたい、セキュリティに配慮したコードを書きたいと考えている方にとって、Fuzzingは強力な味方になってくれます。一緒に学んでいきましょう!

ファズテストの書き方

要件

ファズテストが従わなければならないルールは以下の通りです。

  • ファズテストは FuzzXxx のような名前の関数でなければならず、*testing.F のみを受け取り、戻り値はありません。
  • ファズテストを実行するには、*_test.go ファイルに記述する必要があります。
  • ファズターゲットは、(*testing.F).Fuzz のメソッド呼び出しでなければならず、最初のパラメータとして *testing.T を受け取り、その後にファジング引数が続きます。戻り値はありません。
  • ファズテストごとに、ファズターゲットは正確に1つでなければなりません。
  • すべてのシードコーパスエントリは、ファジング引数と同じ型を同じ順序で持つ必要があります。これは (*testing.F).Add の呼び出しと、ファズテストの testdata/fuzz ディレクトリ内のすべてのコーパスファイルに当てはまります。
  • ファジング引数には、以下の型のみが使用できます:
    • string, []byte
    • int, int8, int16, int32/rune, int64
    • uint, uint8/byte, uint16, uint32, uint64
    • float32, float64
    • bool

解説

ファズテストの基本ルール

ファズテストを書くには、いくつかの決まりごとがあります。一つずつ見ていきましょう。

ルール1: 関数名は FuzzXxx で始める

通常のテスト関数が TestXxx で始まるように、ファズテストは FuzzXxx で始める必要があります。

// ✅ 正しい例
func FuzzUsername(f *testing.F) {
    // ファズテストのコード
}

// ❌ 間違い - Fuzzで始まっていない
func TestUsername(f *testing.F) {
    // これは普通のテスト
}

ポイント: 引数は *testing.F 型で、戻り値はありません。

ルール2: *_test.go ファイルに書く

これは通常のテストと同じです。

myapp/
  ├── user.go
  └── user_test.go  ← ここにファズテストを書く

ルール3: ファズターゲットは1つだけ

ファズターゲットとは、実際にランダムな入力でテストされる部分のことです。

f.Fuzz() というメソッドを使って定義しますが、1つのファズテストに1つだけ書けます。

func FuzzExample(f *testing.F) {
    // シードコーパスの追加
    f.Add("test")
    
    // ファズターゲット(これは1つだけ)
    f.Fuzz(func(t *testing.T, input string) {
        // ここでテストするコード
    })
    
    // ❌ 2つ目のf.Fuzz()は書けない!
}

ルール4: シードコーパスとファジング引数の型を揃える

シードコーパスとは、ファジングの「出発点」となる初期データのことです。

型が一致していないとエラーになります:

func FuzzCalculate(f *testing.F) {
    // ファズターゲットは (int, int) を受け取る
    f.Fuzz(func(t *testing.T, a int, b int) {
        // テストコード
    })
    
    // ✅ 正しい - 型が一致
    f.Add(10, 20)
    f.Add(0, 0)
    f.Add(-5, 100)
    
    // ❌ 間違い - 型が一致しない
    f.Add("10", "20")  // stringはダメ
    f.Add(10)          // 引数の数が違う
}

ルール5: 使える型は限られている

ファジング引数に使える型は、以下のものだけです:

文字列・バイト列

f.Fuzz(func(t *testing.T, s string) { /* ... */ })
f.Fuzz(func(t *testing.T, b []byte) { /* ... */ })

整数型(符号あり)

f.Fuzz(func(t *testing.T, n int) { /* ... */ })
f.Fuzz(func(t *testing.T, n int32) { /* ... */ })
f.Fuzz(func(t *testing.T, r rune) { /* ... */ })  // runeはint32の別名

整数型(符号なし)

f.Fuzz(func(t *testing.T, n uint) { /* ... */ })
f.Fuzz(func(t *testing.T, b byte) { /* ... */ })  // byteはuint8の別名

浮動小数点数

f.Fuzz(func(t *testing.T, x float64) { /* ... */ })

真偽値

f.Fuzz(func(t *testing.T, flag bool) { /* ... */ })

なぜこの型だけなのか?

Fuzzingエンジンは、これらの基本的な型に対して効率的にランダムな値を生成できます。

使えない型の例:

  • ❌ 構造体 (struct)
  • ❌ マップ (map)
  • ❌ スライス([]byte以外)
  • ❌ インターフェース
  • ❌ ポインタ

もし複雑な型をテストしたい場合は、基本型から組み立てる必要があります:

// ❌ 直接構造体は使えない
type User struct {
    Name string
    Age  int
}

// ✅ 基本型で受け取って構造体を作る
func FuzzUser(f *testing.F) {
    f.Add("alice", 25)
    
    f.Fuzz(func(t *testing.T, name string, age int) {
        user := User{Name: name, Age: age}
        // userを使ったテスト
    })
}

実際の例: 完全なファズテスト

package main

import "testing"

func FuzzReverse(f *testing.F) {
    // シードコーパス: 初期テストケース
    f.Add("hello")
    f.Add("Go")
    f.Add("")
    
    // ファズターゲット: ここが実際にテストされる
    f.Fuzz(func(t *testing.T, input string) {
        // 文字列を逆にする関数をテスト
        reversed := Reverse(input)
        
        // もう一度逆にしたら元に戻るはず
        doubleReversed := Reverse(reversed)
        
        if input != doubleReversed {
            t.Errorf("Reverse(Reverse(%q)) = %q, want %q", 
                input, doubleReversed, input)
        }
    })
}

まとめ

ファズテストを書くときのチェックリスト:

  • ✅ 関数名は FuzzXxx で始まっているか?
  • *_test.go ファイルに書いているか?
  • f.Fuzz() は1つだけか?
  • ✅ シードコーパス(f.Add())の型は正しいか?
  • ✅ ファジング引数は許可された型だけか?

これらのルールを守れば、Goのファジングエンジンが自動的に何千もの入力パターンを試してバグを見つけてくれます!

おわりに 

本日は、Go言語のFuzzingについて解説しました。

よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました