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

スポンサーリンク
Go言語入門:Fuzzing -Vol.3- ノウハウ
Go言語入門:Fuzzing -Vol.3-
この記事は約9分で読めます。
よっしー
よっしー

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

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

スポンサーリンク

背景

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

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

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

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

ファズテストの書き方

推奨事項

以下は、ファジングを最大限に活用するための推奨事項です。

  • ファズターゲットは高速かつ決定論的であるべきです。そうすることで、ファジングエンジンが効率的に動作し、新しい失敗やコードカバレッジを容易に再現できます。
  • ファズターゲットは複数のワーカー間で並列に、かつ非決定論的な順序で呼び出されるため、ファズターゲットの状態は各呼び出しの終了後に保持されるべきではなく、ファズターゲットの動作はグローバルな状態に依存すべきではありません。

解説

ファジングを効果的に使うための2つの重要なポイント

ファジングを最大限に活用するには、テストの書き方にちょっとしたコツがあります。難しくないので、一つずつ見ていきましょう。


ポイント1: 高速かつ決定論的に書く

「高速」とは?

ファジングは何千、何万回もテストを実行します。1回のテストに1秒かかったら、とんでもなく時間がかかってしまいますよね。

// ❌ 遅い例 - ネットワーク通信がある
func FuzzAPI(f *testing.F) {
    f.Fuzz(func(t *testing.T, input string) {
        // 実際のAPIを呼び出す(遅い!)
        resp, _ := http.Get("https://api.example.com/check?q=" + input)
        // テストコード...
    })
}

// ✅ 速い例 - ローカルな関数だけ
func FuzzValidate(f *testing.F) {
    f.Fuzz(func(t *testing.T, input string) {
        // 純粋な関数を呼び出す(速い!)
        result := ValidateInput(input)
        // テストコード...
    })
}

避けるべきもの:

  • ネットワーク通信(HTTP、データベース接続)
  • ファイルI/O(大量の読み書き)
  • スリープや待機処理
  • 重い計算処理

「決定論的」とは?

決定論的とは、同じ入力なら必ず同じ結果になるということです。

// ❌ 非決定論的 - 実行するたびに結果が変わる
func FuzzBad(f *testing.F) {
    f.Fuzz(func(t *testing.T, input string) {
        // 現在時刻を使っている(毎回変わる!)
        timestamp := time.Now().Unix()
        result := ProcessWithTime(input, timestamp)
        
        // ランダム値を使っている(毎回変わる!)
        randomValue := rand.Intn(100)
        // ...
    })
}

// ✅ 決定論的 - 同じ入力なら同じ結果
func FuzzGood(f *testing.F) {
    f.Fuzz(func(t *testing.T, input string) {
        // 入力だけに依存する
        result := Process(input)
        
        if !IsValid(result) {
            t.Error("invalid result")
        }
    })
}

なぜ重要なのか?

ファジングがバグを見つけたとき、そのバグを再現できないと困りますよね。決定論的なコードなら、同じ入力で何度でもバグを再現できます。


ポイント2: 状態を持たせない・共有しない

「状態を保持しない」とは?

各テストは独立していて、前回の実行結果を覚えていてはいけません。

// ❌ 悪い例 - 状態を保持している
var counter int  // グローバル変数

func FuzzBad(f *testing.F) {
    f.Fuzz(func(t *testing.T, input string) {
        counter++  // 前回の値を引き継いでしまう
        if counter > 100 {
            t.Error("too many calls")
        }
    })
}

// ✅ 良い例 - 状態を保持しない
func FuzzGood(f *testing.F) {
    f.Fuzz(func(t *testing.T, input string) {
        // 毎回新しく変数を作る
        localCounter := 0
        
        // inputだけに基づいて処理
        result := Process(input)
        // ...
    })
}

なぜ状態を持たせてはいけないのか?

ファズテストは並列実行されます。つまり、複数のテストが同時に走ります。

レストランで例えると:

❌ 悪い例: 1つの注文票を複数の店員が共有
店員A: 「3番の注文を処理中...」
店員B: 「3番の注文を処理中...」← 同じ注文を2回処理してしまう!

✅ 良い例: 各店員が独自の注文票を持つ
店員A: 「私の3番の注文を処理」
店員B: 「私の5番の注文を処理」← 独立して動ける

プログラムで書くと:

// ❌ グローバル変数を使う(並列実行で競合する)
var cache = make(map[string]int)

func FuzzBad(f *testing.F) {
    f.Fuzz(func(t *testing.T, key string) {
        // 複数のgoroutineが同時にアクセス → データ競合!
        cache[key] = len(key)
    })
}

// ✅ ローカル変数を使う(安全)
func FuzzGood(f *testing.F) {
    f.Fuzz(func(t *testing.T, key string) {
        // 毎回新しく作る → 並列実行でも安全
        cache := make(map[string]int)
        cache[key] = len(key)
        // ...
    })
}

「非決定論的な順序」とは?

テストがどの順番で実行されるか分からないということです。

// ❌ 実行順序に依存している
var step int

func FuzzBad(f *testing.F) {
    f.Fuzz(func(t *testing.T, input string) {
        if step == 0 {
            // 最初の実行だと仮定
        } else {
            // 2回目以降だと仮定
        }
        step++
    })
}

並列実行では、テストの実行順序は保証されません:

実行1: テストA → テストB → テストC
実行2: テストC → テストA → テストB  ← 順序が違う!

実践例: 良いファズテストの書き方

package main

import "testing"

// ✅ 理想的なファズテスト
func FuzzParseEmail(f *testing.F) {
    // シードコーパス
    f.Add("user@example.com")
    
    f.Fuzz(func(t *testing.T, email string) {
        // ✅ 高速: ネットワークやI/Oなし
        // ✅ 決定論的: emailだけに依存
        // ✅ 状態なし: グローバル変数を使わない
        
        parsed := ParseEmail(email)
        
        // 単純な検証
        if parsed.Valid {
            if parsed.Username == "" {
                t.Error("valid email must have username")
            }
            if parsed.Domain == "" {
                t.Error("valid email must have domain")
            }
        }
    })
}

もしデータベースやAPIをテストしたい場合は?

ファジングではなく、通常のテストを使いましょう:

// ファジング用: 高速な純粋関数のテスト
func FuzzValidateUser(f *testing.F) {
    f.Fuzz(func(t *testing.T, name string, age int) {
        err := ValidateUser(name, age)
        // 検証...
    })
}

// 通常のテスト: 外部リソースを使う統合テスト
func TestSaveUserToDB(t *testing.T) {
    db := setupTestDB()
    defer db.Close()
    
    user := User{Name: "Alice", Age: 25}
    err := SaveUser(db, user)
    // 検証...
}

まとめ

ファジングを効果的に使うためのチェックリスト:

高速に保つ:

  • ✅ 純粋な関数をテストする
  • ❌ ネットワーク通信、ファイルI/O、データベースアクセスは避ける

決定論的に保つ:

  • ✅ 同じ入力なら必ず同じ結果
  • time.Now(), rand.Intn(), 外部状態への依存を避ける

独立性を保つ:

  • ✅ 各テストは独立して実行できる
  • ❌ グローバル変数、共有状態、実行順序への依存を避ける

これらのルールを守ることで、ファジングエンジンが最大限の効率でバグを見つけてくれます!

おわりに 

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

よっしー
よっしー

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

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

コメント

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