Fuzzy Testing di Go

Background dan Sejarah

Fuzzy testing (testing acak) bukanlah konsep baru dalam dunia software development. Namun, di Go, fitur ini secara resmi diperkenalkan dalam versi 1.18 sebagai bagian dari paket testing.

Latar Belakang:

  • Traditional testing seringkali hanya mengcover kasus-kasus yang sudah diketahui developer

  • Bug yang kompleks sering muncul dari input yang tidak terduga

  • Go ingin menyediakan tools built-in untuk menemukan edge cases secara otomatis

  • Terinspirasi dari tools seperti American Fuzzy Lop (AFL) dan libFuzzer

Apa itu Fuzzy Testing?

Fuzzy testing adalah teknik testing otomatis yang menghasilkan input acak untuk menemukan bug, crash, atau perilaku tak terduga dalam kode. Berbeda dengan unit test yang menggunakan input tetap, fuzzing menggunakan input yang di-generate secara dinamis.

Cara Menggunakan Fuzzy Testing di Go

1. Persiapan

Pastikan menggunakan Go versi 1.18 atau lebih baru:

go version

2. Struktur Dasar Fuzzy Test

Buat file fuzz_test.go:

package main

import (
    "encoding/hex"
    "testing"
    "unicode/utf8"
)

// Function yang akan di-test
func ReverseString(s string) (string, error) {
    if !utf8.ValidString(s) {
        return "", &InvalidStringError{Input: s}
    }
    runes := []rune(s)
    for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
        runes[i], runes[j] = runes[j], runes[i]
    }
    return string(runes), nil
}

type InvalidStringError struct {
    Input string
}

func (e *InvalidStringError) Error() string {
    return "invalid UTF-8 string: " + hex.EncodeToString([]byte(e.Input))
}

// Fuzzy test
func FuzzReverseString(f *testing.F) {
    // Seed corpus - contoh input awal
    testcases := []string{"Hello", " ", "!12345", "こんにちは"}
    for _, tc := range testcases {
        f.Add(tc)  // Menambahkan seed corpus
    }

    f.Fuzz(func(t *testing.T, orig string) {
        rev, err := ReverseString(orig)
        if err != nil {
            // Skip invalid UTF-8 strings jika function kita tidak mendukungnya
            return
        }
        
        revAgain, err := ReverseString(rev)
        if err != nil {
            t.Fatalf("Failed to reverse again: %v", err)
        }
        
        if orig != revAgain {
            t.Errorf("Before: %q, after: %q", orig, revAgain)
        }
        
        // Pastikan hasil reverse masih valid UTF-8
        if !utf8.ValidString(rev) {
            t.Errorf("Reverse produced invalid UTF-8: %q", rev)
        }
    })
}

3. Menjalankan Fuzzy Test

# Jalankan fuzzy test dengan default settings
go test -fuzz=FuzzReverseString

# Jalankan dengan timeout tertentu
go test -fuzz=FuzzReverseString -fuzztime=30s

# Jalankan dengan worker tertentu
go test -fuzz=FuzzReverseString -parallel=4

# Hanya jalankan seed corpus (seperti unit test biasa)
go test -run=FuzzReverseString

4. Analisis Hasil Crash

Jika fuzzing menemukan bug, Go akan menyimpan input yang menyebabkan crash:

# Setelah menemukan crash, kita bisa debug dengan:
go test -run=^FuzzReverseString/fuzzme12345$ -v

Best Practices dan Cara Pakai yang Benar

1. Design Test yang Baik

func FuzzProcessing(f *testing.F) {
    // Tambahkan berbagai jenis seed
    f.Add("normal input")
    f.Add("")
    f.Add("very long string...")
    f.Add("special chars !@#$%^&*()")
    
    f.Fuzz(func(t *testing.T, input string) {
        result, err := ProcessInput(input)
        
        // Jangan panic, handle error dengan proper
        if err != nil {
            // Untuk input yang memang diharapkan gagal, cukup return
            return
        }
        
        // Validasi invariants (hal yang harus selalu true)
        if result != "" && !isValid(result) {
            t.Errorf("Invalid result: %v", result)
        }
    })
}

2. Handling Complex Data Types

// Custom type untuk fuzzing
func FuzzUserValidation(f *testing.F) {
    // Seed dengan berbagai usia
    f.Add("John Doe", 25, "john@example.com")
    f.Add("", -1, "invalid-email")
    f.Add("Very Long Name", 150, "a@b.c")
    
    f.Fuzz(func(t *testing.T, name string, age int, email string) {
        user := User{
            Name:  name,
            Age:   age,
            Email: email,
        }
        
        err := user.Validate()
        // Tidak semua input harus valid, test bagaimana code handle invalid input
        if err != nil && !isExpectedError(err) {
            t.Errorf("Unexpected error: %v for input: %+v", err, user)
        }
    })
}

3. Performance Considerations

func FuzzPerformanceCritical(f *testing.F) {
    f.Add(1000) // size parameter
    
    f.Fuzz(func(t *testing.T, size int) {
        // Batasi size untuk menghindari OOM
        if size < 0 || size > 10000 {
            return
        }
        
        data := generateLargeData(size)
        start := time.Now()
        
        result := ProcessData(data)
        
        // Test performance boundary
        if time.Since(start) > 100*time.Millisecond {
            t.Errorf("Processing took too long: %v", time.Since(start))
        }
        
        if result == nil {
            t.Error("Unexpected nil result")
        }
    })
}

Real-World Use Cases

1. Testing Parser dan Validator

// JSON parser fuzzing
func FuzzJSONParser(f *testing.F) {
    f.Add(`{"name": "test", "value": 123}`)
    f.Add(`invalid json`)
    f.Add(`{"nested": {"deep": [1,2,3]}}`)
    
    f.Fuzz(func(t *testing.T, jsonStr string) {
        var data map[string]interface{}
        err := json.Unmarshal([]byte(jsonStr), &data)
        
        // Test bahwa parser tidak panic pada input arbitrary
        if err != nil {
            // Expected error for invalid JSON
            return
        }
        
        // Test bahwa valid JSON dapat diproses lebih lanjut
        processed, err := ProcessJSON(data)
        if err != nil {
            t.Errorf("Failed to process valid JSON: %v", err)
        }
        
        if processed == nil {
            t.Error("Unexpected nil processing result")
        }
    })
}

2. Security Testing

// SQL injection detection fuzzing
func FuzzSQLInjectionDetection(f *testing.F) {
    f.Add("normal input")
    f.Add("' OR 1=1 --")
    f.Add("; DROP TABLE users; --")
    
    f.Fuzz(func(t *testing.T, input string) {
        isMalicious := DetectSQLInjection(input)
        
        // Test bahwa detector tidak false positive/negative
        if containsSQLKeywords(input) && !isMalicious {
            t.Errorf("Possible false negative for: %q", input)
        }
        
        // Untuk input normal, harus tidak terdeteksi sebagai malicious
        if isNormalInput(input) && isMalicious {
            t.Errorf("False positive for: %q", input)
        }
    })
}

3. Network Protocol Testing

// Custom protocol fuzzing
func FuzzProtocolParser(f *testing.F) {
    // Seed dengan valid protocol messages
    f.Add([]byte{0x01, 0x02, 0x03, 0x04})
    f.Add([]byte{})
    f.Add(make([]byte, 1000)) // large message
    
    f.Fuzz(func(t *testing.T, data []byte) {
        defer func() {
            if r := recover(); r != nil {
                t.Errorf("Parser panicked with input: %v", data)
            }
        }()
        
        message, err := ParseProtocolMessage(data)
        if err != nil {
            // Expected for invalid messages
            return
        }
        
        // Test round-trip consistency
        reconstructed := message.Serialize()
        if !bytes.Equal(data, reconstructed) {
            t.Errorf("Round-trip mismatch: %v vs %v", data, reconstructed)
        }
    })
}

4. Configuration Validation

// Config validation fuzzing
func FuzzConfigValidation(f *testing.F) {
    f.Add(`{"timeout": 1000, "retries": 3}`)
    f.Add(`{"timeout": -1, "retries": 100}`)
    f.Add(`invalid config`)
    
    f.Fuzz(func(t *testing.T, configStr string) {
        var config Config
        err := json.Unmarshal([]byte(configStr), &config)
        if err != nil {
            return // Invalid JSON
        }
        
        err = config.Validate()
        if err != nil && !isExpectedValidationError(err) {
            t.Errorf("Unexpected validation error: %v", err)
        }
        
        // Test bahwa valid config dapat digunakan
        if err == nil {
            service := NewService(config)
            if service == nil {
                t.Error("Failed to create service with valid config")
            }
        }
    })
}

Tips dan Tricks

  1. Start Small: Mulai dengan seed corpus yang kecil dan representatif

  2. Progressively Complex: Tambahkan complexity secara bertahap

  3. Monitor Resources: Fuzzy test bisa consume banyak memory dan CPU

  4. CI Integration: Jadwalkan fuzzy test di CI/CD pipeline

  5. Corpus Management: Go otomatis menyimpan interesting inputs di directory testdata/fuzz

Kesimpulan

Fuzzy testing di Go 1.18+ adalah tools powerful untuk:

  • Menemukan edge cases dan bug yang tidak terduga

  • Meningkatkan robustness dan security code

  • Mengotomatiskan discovery of new test cases

  • Melengkapi traditional unit dan integration testing

Dengan approach yang sistematis, fuzzy testing dapat menjadi bagian valuable dari testing strategy di project Go modern.

Last updated