Unit test hanya memeriksa input yang sudah kita bayangkan, padahal bug sering bersembunyi pada input tak terpikirkan: string kosong, karakter Unicode aneh, atau byte rusak. Fuzz testing membalik arah kerjanya: kita menuliskan property yang harus selalu benar, lalu mesin go test -fuzz menghasilkan ribuan input acak untuk mencobanya. Fasilitas ini tersedia sejak Go 1.18 tanpa library eksternal. Pada chapter ini kita menguji fungsi pembalik string yang benar, lalu melihat fuzzer menghukum versi yang bermasalah.
Konsep Fuzz Testing #
Target fuzz ditulis sebagai fungsi FuzzXxx(f *testing.F) pada berkas _test.go. Tipe testing.F menyediakan f.Add untuk mendaftarkan seed corpus (contoh input awal yang dijamin dijalankan) dan f.Fuzz untuk mendaftarkan fungsi pemeriksaan. Mesin lalu memutasi seed — mengganti, menambah, membuang byte — sambil mengamati kegagalan atau jalur kode baru.
Fungsi pemeriksaan tidak membandingkan keluaran tetap, melainkan property yang berlaku untuk semua input — misalnya jumlah karakter tidak berubah dan membalik dua kali mengembalikan input asli. Saat go test biasa berjalan, seluruh seed corpus tetap dieksekusi sebagai unit test.
Fungsi yang Diuji #
Kita menguji BalikKata, fungsi yang membalik urutan huruf dengan benar melalui konversi ke []rune. Simpan kode berikut sebagai balik.go:
package main
import "fmt"
// BalikKata membalik urutan huruf sebuah kata atau kalimat.
// Konversi ke []rune membuat karakter multi-byte seperti "é" tetap utuh,
// bukan terpotong per byte.
func BalikKata(s string) string {
huruf := []rune(s)
for i, j := 0, len(huruf)-1; i < j; i, j = i+1, j-1 {
huruf[i], huruf[j] = huruf[j], huruf[i]
}
return string(huruf)
}
func main() {
fmt.Println(BalikKata("katak"))
fmt.Println(BalikKata("belajar go"))
fmt.Println(BalikKata("résumé"))
}Jalankan berkas tersebut dari folder chapter:
go run balik.goOutput yang Dihasilkan:
katak
og rajaleb
émusérMari kita telaah baris demi baris kode di atas:
- Baris 9 (
huruf := []rune(s)): mengubah string menjadi potongan rune agar pembalikan bekerja pada karakter, bukan byte. - Baris 10-12 (loop
for): menukar huruf pertama dengan terakhir, kedua dengan kedua-dari-belakang, dan seterusnya sampai kedua penunjuk bertemu di tengah. - Baris 13 (
return string(huruf)): merakit kembali potongan rune menjadi string. - Baris 16-19 (
func main): mencetak tiga contoh, termasuk kata dengan huruf beraksen"résumé"yang hanya benar bila pembalikan berbasis rune.
Menulis Target Fuzz #
Berkas balik_test.go berisi unit test biasa dan satu target fuzz:
package main
import (
"testing"
"unicode/utf8"
)
func TestBalikKata(t *testing.T) {
uji := []struct {
masuk string
diharapkan string
}{
{"katak", "katak"},
{"go", "og"},
{"hijau", "uajih"},
{"résumé", "émusér"},
}
for _, u := range uji {
if got := BalikKata(u.masuk); got != u.diharapkan {
t.Errorf("BalikKata(%q) = %q, diharapkan %q", u.masuk, got, u.diharapkan)
}
}
}
func FuzzBalikKata(f *testing.F) {
f.Add("katak")
f.Add("belajar go")
f.Add("résumé")
f.Fuzz(func(t *testing.T, s string) {
hasil := BalikKata(s)
if len([]rune(hasil)) != len([]rune(s)) {
t.Fatalf("jumlah rune berubah: masuk %d, keluar %d", len([]rune(s)), len([]rune(hasil)))
}
if utf8.ValidString(s) && BalikKata(hasil) != s {
t.Errorf("BalikKata dua kali harus mengembalikan input asli, input %q menghasilkan %q", s, BalikKata(hasil))
}
})
}Perhatikan bagian-bagian pentingnya:
- Baris 8-23 (
func TestBalikKata): table-driven test untuk kasus yang diketahui; fuzzing bukan pengganti unit test. - Baris 26-28 (
f.Add): seed corpus berisi tiga input tetap. Seed ini dijalankan setiapgo testbiasa dan menjadi bahan mutasi mesin. - Baris 31-33 (property pertama): jumlah rune keluaran tidak boleh berbeda dari jumlah rune input. Pemeriksaan ini berlaku juga untuk string berisi byte UTF-8 rusak.
- Baris 34-36 (property kedua):
BalikKata(BalikKata(s))harus mengembalikan input asli. Pemeriksaan dijaga denganutf8.ValidStringkarena membalik byte rusak memang tidak dapat dipulihkan.
Menjalankan Fuzzing #
Perintah berikut menjalankan mesin fuzzing untuk satu target selama lima iterasi agar selesai cepat:
go test -fuzz=FuzzBalikKata -fuzztime=5xOutput yang Dihasilkan:
fuzz: elapsed: 0s, gathering baseline coverage: 0/3 completed
fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed, now fuzzing with 2 workers
fuzz: elapsed: 0s, execs: 5 (1234/sec), new interesting: 0 (total: 3)
PASS
ok tutorial-website/content/go/testing/fuzz-testing 0.006sBaris-baris output tersebut memiliki arti sebagai berikut:
| Bagian output | Arti |
|---|---|
gathering baseline coverage: 0/3 → 3/3 | Seed corpus dijalankan dulu untuk mengukur cakupan awal |
now fuzzing with 2 workers | Mesin mutasi berjalan pada proses worker, sesuai jumlah CPU |
execs: 5 | Lima iterasi mutasi telah dieksekusi (-fuzztime=5x) |
new interesting: 0 (total: 3) | Input baru yang menemukan jalur kode baru, ditambah ke corpus di cache |
Ketika Fuzzer Menemukan Bug #
Untuk melihat kegagalan yang ditemukan fuzzing, kita siapkan versi bermasalah yang membalik per byte — benar untuk ASCII, korup untuk karakter multi-byte:
// BalikKataByte membalik per byte: benar untuk ASCII, korup untuk UTF-8.
func BalikKataByte(s string) string {
hasil := []byte(s)
for i, j := 0, len(hasil)-1; i < j; i, j = i+1, j-1 {
hasil[i], hasil[j] = hasil[j], hasil[i]
}
return string(hasil)
}Versi bermasalah tersebut dijalankan di folder percobaan terpisah dengan property: input UTF-8 valid harus tetap valid setelah dibalik (nama package-nya tampil sebagai scratch14b/buggy). Fuzzer menemukan pelanggar:
go test -fuzz=FuzzBalikKataByte -fuzztime=20000xOutput yang Dihasilkan:
fuzz: elapsed: 0s, gathering baseline coverage: 0/16 completed
fuzz: elapsed: 0s, gathering baseline coverage: 16/16 completed, now fuzzing with 2 workers
fuzz: minimizing 29-byte failing input file
fuzz: elapsed: 0s, minimizing
--- FAIL: FuzzBalikKataByte (0.03s)
--- FAIL: FuzzBalikKataByte (0.00s)
balikbug_test.go:12: input UTF-8 valid menjadi tidak valid: "ŗ"
Failing input written to testdata/fuzz/FuzzBalikKataByte/01c1047d88ffbd57
To re-run:
go test -run=FuzzBalikKataByte/01c1047d88ffbd57
FAIL
exit status 1
FAIL scratch14b/buggy 0.030sMesin fuzzing menyimpan input yang gagal sebagai berkas di testdata/fuzz/FuzzBalikKataByte/ dengan isi seperti berikut:
go test fuzz v1
string("ŗ")Kata minimizing berarti fuzzer menyusutkan input gagal 29 byte tadi sampai paling kecil yang masih menggagalkan test — hasilnya rune tunggal ŗ. Berkasnya berformat go test fuzz v1: baris pertama menandai jenis, baris berikutnya menuliskan nilai input memakai sintaks literal Go. Setelah berkas ini ada, go test -run='FuzzBalikKataByte/01c1047d88ffbd57' menjalankan ulang input gagal sebagai test regresi — kegagalan terdeteksi selamanya tanpa mesin fuzzing.
Dengan property yang cermat, fuzzing melengkapi unit test sebagai jaring pengaman input tak terduga. Chapter berikutnya membahas cara lain memanfaatkan berkas _test.go: contoh penggunaan yang sekaligus dokumentasi terverifikasi.
Referensi #
- Go Security — Fuzzing
— panduan resmi fuzzing: seed corpus, crasher, dan flag
-fuzztime. - Tipe testing.F
— API
f.Adddanf.Fuzz. - Blog: Fuzzing is ready for beta — pengantar desain fuzzing bawaan Go.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
balik_test.go
Unduh berkaspackage main
import (
"testing"
"unicode/utf8"
)
func TestBalikKata(t *testing.T) {
uji := []struct {
masuk string
diharapkan string
}{
{"katak", "katak"},
{"go", "og"},
{"hijau", "uajih"},
{"résumé", "émusér"},
}
for _, u := range uji {
if got := BalikKata(u.masuk); got != u.diharapkan {
t.Errorf("BalikKata(%q) = %q, diharapkan %q", u.masuk, got, u.diharapkan)
}
}
}
func FuzzBalikKata(f *testing.F) {
f.Add("katak")
f.Add("belajar go")
f.Add("résumé")
f.Fuzz(func(t *testing.T, s string) {
hasil := BalikKata(s)
if len([]rune(hasil)) != len([]rune(s)) {
t.Fatalf("jumlah rune berubah: masuk %d, keluar %d", len([]rune(s)), len([]rune(hasil)))
}
if utf8.ValidString(s) && BalikKata(hasil) != s {
t.Errorf("BalikKata dua kali harus mengembalikan input asli, input %q menghasilkan %q", s, BalikKata(hasil))
}
})
}
balik.go
Unduh berkaspackage main
import "fmt"
// BalikKata membalik urutan huruf sebuah kata atau kalimat.
// Konversi ke []rune membuat karakter multi-byte seperti "é" tetap utuh,
// bukan terpotong per byte.
func BalikKata(s string) string {
huruf := []rune(s)
for i, j := 0, len(huruf)-1; i < j; i, j = i+1, j-1 {
huruf[i], huruf[j] = huruf[j], huruf[i]
}
return string(huruf)
}
func main() {
fmt.Println(BalikKata("katak"))
fmt.Println(BalikKata("belajar go"))
fmt.Println(BalikKata("résumé"))
}