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:

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:

bash
go run balik.go

Output yang Dihasilkan:

text
katak
og rajaleb
émusér

Mari kita telaah baris demi baris kode di atas:

  1. Baris 9 (huruf := []rune(s)): mengubah string menjadi potongan rune agar pembalikan bekerja pada karakter, bukan byte.
  2. Baris 10-12 (loop for): menukar huruf pertama dengan terakhir, kedua dengan kedua-dari-belakang, dan seterusnya sampai kedua penunjuk bertemu di tengah.
  3. Baris 13 (return string(huruf)): merakit kembali potongan rune menjadi string.
  4. Baris 16-19 (func main): mencetak tiga contoh, termasuk kata dengan huruf beraksen "résumé" yang hanya benar bila pembalikan berbasis rune.

Berkas balik_test.go berisi unit test biasa dan satu target fuzz:

go
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:

  1. Baris 8-23 (func TestBalikKata): table-driven test untuk kasus yang diketahui; fuzzing bukan pengganti unit test.
  2. Baris 26-28 (f.Add): seed corpus berisi tiga input tetap. Seed ini dijalankan setiap go test biasa dan menjadi bahan mutasi mesin.
  3. 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.
  4. Baris 34-36 (property kedua): BalikKata(BalikKata(s)) harus mengembalikan input asli. Pemeriksaan dijaga dengan utf8.ValidString karena membalik byte rusak memang tidak dapat dipulihkan.

Menjalankan Fuzzing #

Perintah berikut menjalankan mesin fuzzing untuk satu target selama lima iterasi agar selesai cepat:

bash
go test -fuzz=FuzzBalikKata -fuzztime=5x

Output yang Dihasilkan:

text
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.006s

Baris-baris output tersebut memiliki arti sebagai berikut:

Bagian outputArti
gathering baseline coverage: 0/3 → 3/3Seed corpus dijalankan dulu untuk mengukur cakupan awal
now fuzzing with 2 workersMesin mutasi berjalan pada proses worker, sesuai jumlah CPU
execs: 5Lima 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:

go
// 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:

bash
go test -fuzz=FuzzBalikKataByte -fuzztime=20000x

Output yang Dihasilkan:

text
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.030s

Mesin fuzzing menyimpan input yang gagal sebagai berkas di testdata/fuzz/FuzzBalikKataByte/ dengan isi seperti berikut:

text
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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

balik_test.go

Unduh berkas
go
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))
		}
	})
}

balik.go

Unduh berkas
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é"))
}