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
Start Small: Mulai dengan seed corpus yang kecil dan representatif
Progressively Complex: Tambahkan complexity secara bertahap
Monitor Resources: Fuzzy test bisa consume banyak memory dan CPU
CI Integration: Jadwalkan fuzzy test di CI/CD pipeline
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