Fungsi nyata jarang punya satu perilaku: fungsi validasi menampik berbagai bentuk masukan yang salah, fungsi formatter menghasilkan teks berbeda untuk tiap kategori angka, dan fungsi pemrosesan string berperilaku khusus di batas array. Menulis satu fungsi test per kasus membuat berkas test membengkak dengan salin-tempel yang hampir identik. Go punya jawaban idiomatik untuk masalah tersebut: table-driven test, pola yang menghimpun seluruh kasus ke dalam satu tabel lalu memeriksa kasus demi kasus di dalam satu loop. Pada chapter ini kita menerapkan pola tersebut pada permainan klasik FizzBuzz, lalu kita amati kekuatannya ketika test gagal.
Kode Produksi FizzBuzz #
Aturan FizzBuzz sederhana namun kaya cabang: bilangan habis dibagi 3 menghasilkan "Fizz", habis dibagi 5 menghasilkan "Buzz", habis dibagi keduanya menghasilkan "FizzBuzz", dan bilangan lain menghasilkan teks angkanya. Banyaknya cabang menjadikan FizzBuzz contoh ideal untuk tabel kasus:
package main
import "strconv"
// FizzBuzz mengembalikan "Fizz" bila n habis dibagi 3, "Buzz" bila n
// habis dibagi 5, "FizzBuzz" bila n habis dibagi keduanya, dan teks
// angkanya sendiri bila n tidak habis dibagi 3 maupun 5.
func FizzBuzz(n int) string {
switch {
case n%15 == 0:
return "FizzBuzz"
case n%3 == 0:
return "Fizz"
case n%5 == 0:
return "Buzz"
default:
return strconv.Itoa(n)
}
}Fungsi FizzBuzz memilih hasil lewat switch tanpa ekspresi: setiap case dievaluasi dari atas ke bawah, sehingga n%15 == 0 harus diperiksa lebih dulu agar kasus kelipatan 15 tidak tertelan cabang lain.
Tabel Kasus dalam Satu Test #
Seluruh kasus uji dideklarasikan sebagai slice berisi struct anonim. Setiap elemen tabel memuat nama kasus, masukan, dan hasil yang diharapkan:
package main
import "testing"
func TestFizzBuzz(t *testing.T) {
cases := []struct {
nama string
input int
want string
}{
{"angka biasa", 1, "1"},
{"angka tujuh", 7, "7"},
{"habis dibagi tiga", 3, "Fizz"},
{"habis dibagi lima", 5, "Buzz"},
{"habis dibagi keduanya", 15, "FizzBuzz"},
{"kelipatan tiga besar", 90, "FizzBuzz"},
}
for _, c := range cases {
got := FizzBuzz(c.input)
if got != c.want {
t.Errorf("%s: FizzBuzz(%d) = %q; want %q", c.nama, c.input, got, c.want)
}
}
}Mari kita telaah baris demi baris berkas test di atas:
- Baris 6 (
cases := []struct {): mendeklarasikan variabelcasessebagai slice dari struct anonim. Struct tersebut didefinisikan langsung di tempat karena hanya dipakai test ini — tidak perlu nama tipe tersendiri. - Baris 7-9 (field
nama,input,want): menetapkan kolom tabel. Fieldnamamenjelaskan maksud kasus,inputadalah masukan fungsi, danwantadalah hasil yang benar. Trik namawant(dan hasil aktualgot) adalah konvensi Go yang muncul berulang di pesan kegagalan. - Baris 11-16 (enam elemen tabel): mengisi enam kasus yang mewakili seluruh cabang
FizzBuzz— angka biasa, angka tujuh, kelipatan 3, kelipatan 5, kelipatan 15, dan kelipatan besar. Menambah kasus baru cukup dengan menambah satu baris. - Baris 19 (
for _, c := range cases): mengiterasi seluruh kasus; variabel loopcmemegang satu elemen tabel pada tiap putaran. Namacsingkat namun cukup jelas karena isinya memang satu kasus. - Baris 20-23 (
gotdan pemeriksaan): hasil aktual disimpan padagot, lalu dibandingkan denganc.want. Saat tidak cocok,t.Errorfmencatat kegagalan lengkap dengan nama kasus, masukan, hasil aktual, dan hasil harapan — format"got %q want %q"membuat perbedaan langsung terbaca, dan untuk kasus dengan tipe campuran, verb%vadalah pilihan aman yang mencetak nilai bertipe apa pun.
Loop tersebut berjalan enam kali di dalam satu fungsi test bernama TestFizzBuzz. Karena t.Errorf tidak menghentikan eksekusi, satu kasus gagal tidak menyamarkan kasus lain yang juga gagal. Jalankan test dari direktori chapter:
go test -v ./...Output yang Dihasilkan:
=== RUN TestFizzBuzz
--- PASS: TestFizzBuzz (0.00s)
PASSKekuatan Tabel Saat Test Gagal #
Pola tabel paling terasa manfaatnya ketika kode produksi salah. Coba ubah sementara baris 12 fizzbuzz.go dari case n%3 == 0: menjadi case n%3 == 1:, jalankan go test -v ./..., lalu kembalikan. Tiga dari enam kasus gagal sekaligus dan masing-masing dilaporkan dengan namanya:
Output yang Dihasilkan:
=== RUN TestFizzBuzz
fizzbuzz_test.go:22: angka biasa: FizzBuzz(1) = "Fizz"; want "1"
fizzbuzz_test.go:22: angka tujuh: FizzBuzz(7) = "Fizz"; want "7"
fizzbuzz_test.go:22: habis dibagi tiga: FizzBuzz(3) = "3"; want "Fizz"
--- FAIL: TestFizzBuzz (0.00s)
FAILKetiga baris galat berasal dari baris 22 fizzbuzz_test.go — pemanggilan t.Errorf di dalam loop — tetapi nama kasus pada awal pesan (angka biasa, angka tujuh, habis dibagi tiga) langsung menunjukkan cabang mana yang rusak. Tanpa pola tabel, kita perlu tiga fungsi test terpisah untuk diagnosis setanding.
Mengapa Pola Ini Idiomatik #
Table-driven test didokumentasikan pada wiki resmi Go sebagai pola standar penulisan test. Alasannya praktis: menambah kasus baru hanya menambah satu baris tabel tanpa menyentuh logika loop; seluruh kasus berderet layaknya spesifikasi yang bisa dibaca manusia; dan setiap kegagalan terlapor terpisah dengan konteks masukannya. Pola ini juga cocok untuk validasi seperti EmailValid(email string) bool — tabelnya cukup memuat input string dan want bool — serta fungsi yang mengembalikan error dengan menambah kolom wantErr error.
Variasi lanjutan dari pola ini mengganti isi loop dengan t.Run sehingga setiap kasus menjadi subtest bernama: output menjadi lebih rinci, satu kasus dapat dijalankan terpisah, dan kegagalan terlapor dengan nama kasusnya — pembahasan lengkapnya menanti pada chapter Subtest
.
Referensi #
- Dokumentasi package testing
— referensi resmi
t.Errorfdan tipetesting.T. - Wiki Go: TableDrivenTests — penjelasan resmi pola table-driven test beserta contohnya.
- Tutorial: Add a test to a function — tutorial resmi berikut pola tabelnya.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
fizzbuzz_test.go
Unduh berkaspackage main
import "testing"
func TestFizzBuzz(t *testing.T) {
cases := []struct {
nama string
input int
want string
}{
{"angka biasa", 1, "1"},
{"angka tujuh", 7, "7"},
{"habis dibagi tiga", 3, "Fizz"},
{"habis dibagi lima", 5, "Buzz"},
{"habis dibagi keduanya", 15, "FizzBuzz"},
{"kelipatan tiga besar", 90, "FizzBuzz"},
}
for _, c := range cases {
got := FizzBuzz(c.input)
if got != c.want {
t.Errorf("%s: FizzBuzz(%d) = %q; want %q", c.nama, c.input, got, c.want)
}
}
}
fizzbuzz.go
Unduh berkaspackage main
import "strconv"
// FizzBuzz mengembalikan "Fizz" bila n habis dibagi 3, "Buzz" bila n
// habis dibagi 5, "FizzBuzz" bila n habis dibagi keduanya, dan teks
// angkanya sendiri bila n tidak habis dibagi 3 maupun 5.
func FizzBuzz(n int) string {
switch {
case n%15 == 0:
return "FizzBuzz"
case n%3 == 0:
return "Fizz"
case n%5 == 0:
return "Buzz"
default:
return strconv.Itoa(n)
}
}