Satu fungsi test yang memuat banyak pemeriksaan punya kelemahan: ketika gagal, go test hanya menampilkan nama fungsi test tersebut, bukan bagian mana yang bermasalah. Kita harus membaca ulang isi fungsi untuk menemukan cabang yang rusak. Metode t.Run memecah masalah ini dengan cara membagi satu test menjadi beberapa subtest bernama, sehingga setiap bagian dilaporkan, difilter, dan dikelola secara terpisah. Pada chapter ini kita memecah test validasi email menjadi subtest per aturan, menjalankan satu subtest tertentu, lalu mengamati output yang menjorok khas subtest.
Kode Produksi: Validasi Email #
Fungsi EmailValid memeriksa empat aturan format email sederhana. Banyaknya aturan menjadikan fungsi tersebut cocok untuk dipecah menjadi subtest, satu subtest per kelompok aturan:
package main
import "strings"
// EmailValid memeriksa format email sederhana: tepat satu tanda @,
// bagian lokal tidak kosong, dan domain memuat titik serta tidak
// diawali atau diakhiri titik.
func EmailValid(email string) bool {
bagian := strings.Split(email, "@")
if len(bagian) != 2 {
return false
}
lokal, domain := bagian[0], bagian[1]
return lokal != "" &&
strings.Contains(domain, ".") &&
!strings.HasPrefix(domain, ".") &&
!strings.HasSuffix(domain, ".")
}Fungsi EmailValid mengembalikan false apabila salah satu aturan dilanggar: jumlah bagian hasil strings.Split bukan dua, bagian lokal kosong, atau domain tanpa titik maupun berawalan atau berakhiran titik.
Memecah Test dengan t.Run #
Berkas validasi_test.go memanggil t.Run empat kali di dalam satu fungsi test induk TestEmailValid. Setiap pemanggilan menerima nama subtest dan closure yang memuat pemeriksaannya:
package main
import "testing"
func TestEmailValid(t *testing.T) {
t.Run("email-lengkap", func(t *testing.T) {
for _, email := range []string{"[email protected]", "[email protected]"} {
if !EmailValid(email) {
t.Errorf("EmailValid(%q) = false; want true", email)
}
}
})
t.Run("lokal-kosong", func(t *testing.T) {
if EmailValid("@example.com") {
t.Errorf("EmailValid(%q) = true; want false", "@example.com")
}
})
t.Run("tanpa-at", func(t *testing.T) {
if EmailValid("budi.example.com") {
t.Errorf("EmailValid(%q) = true; want false", "budi.example.com")
}
})
t.Run("domain-tanpa-titik", func(t *testing.T) {
if EmailValid("budi@localhost") {
t.Errorf("EmailValid(%q) = true; want false", "budi@localhost")
}
})
}Mari kita telaah baris demi baris berkas test di atas:
- Baris 5 (
func TestEmailValid(t *testing.T)): fungsi test induk.go testmenjalankan fungsi ini; subtest dijalankan dari dalam fungsi induk secara berurutan. - Baris 6 (
t.Run("email-lengkap", ...)): membuka subtest pertama. Nama subtest ditulis sebagai string bebas; pada output, karakter spasi pada nama diganti garis bawah, sehingga kebiasaan menulis nama pendek dengan tanda hubung membuat nama tetap mudah diketik ulang saat memfilter. - Baris 7-11 (loop di dalam subtest): subtest boleh berisi apa pun yang biasa ditulis test, termasuk loop dan beberapa
t.Errorf. Subtestemail-lengkapmemeriksa dua alamat valid sekaligus karena keduanya menguji aturan yang sama. - Baris 14-18 (subtest
lokal-kosong): memeriksa satu kasus gagal: bagian lokal kosong.t.Errorfyang dipanggil di dalam closure ini akan dilaporkan atas nama subtest, bukan atas nama fungsi induk. - Baris 20-30 (
tanpa-atdandomain-tanpa-titik): dua subtest terakhir yang masing-masing menguji satu aturan tersisa. Setiap subtest menerima parametert *testing.Tsendiri, sehingga kegagalan pada satu subtest tidak menghentikan subtest berikutnya.
Jalankan seluruh test dari direktori chapter:
go test -v ./...Output yang Dihasilkan:
=== RUN TestEmailValid
=== RUN TestEmailValid/email-lengkap
=== RUN TestEmailValid/lokal-kosong
=== RUN TestEmailValid/tanpa-at
=== RUN TestEmailValid/domain-tanpa-titik
--- PASS: TestEmailValid (0.00s)
--- PASS: TestEmailValid/email-lengkap (0.00s)
--- PASS: TestEmailValid/lokal-kosong (0.00s)
--- PASS: TestEmailValid/tanpa-at (0.00s)
--- PASS: TestEmailValid/domain-tanpa-titik (0.00s)
PASSPerhatikan bentuk output tersebut: setiap subtest dicetak dengan nama lengkap TestEmailValid/nama-subtest, dan hasil subtest menjorok empat spasi di bawah hasil test induk. Baris --- PASS: TestEmailValid (0.00s) menandakan fungsi induk selesai, sedangkan empat baris di bawahnya adalah hasil tiap subtest.
Menjalankan Satu Subtest #
Flag -run menerima pola TestInduk/subtest untuk menjalankan subtest tertentu saja:
go test -v -run "TestEmailValid/tanpa-at" ./...Output yang Dihasilkan:
=== RUN TestEmailValid
=== RUN TestEmailValid/tanpa-at
--- PASS: TestEmailValid (0.00s)
--- PASS: TestEmailValid/tanpa-at (0.00s)
PASSPola tersebut memakai garis miring sebagai pemisah nama induk dan nama subtest. Bagian sebelum garis miring dicocokkan terhadap nama test induk, bagian setelahnya dicocokkan terhadap nama subtest, dan keduanya adalah regular expression — -run "TestEmailValid/tanpa-.*" menjalankan seluruh subtest yang namanya diawali tanpa-. Kemampuan ini mempercepat perbaikan: cukup jalankan subtest yang sedang rusak tanpa menunggu test lain.
Diagnosis Presisi Saat Subtest Gagal #
Nilai terbesar subtest terlihat saat kegagalan. Ubah sementara baris 15 pada validasi.go dari strings.Contains(domain, ".") && menjadi true &&, jalankan go test -v ./..., lalu kembalikan baris tersebut. Aturan domain tanpa titik tidak lagi berfungsi:
Output yang Dihasilkan:
=== RUN TestEmailValid
=== RUN TestEmailValid/email-lengkap
=== RUN TestEmailValid/lokal-kosong
=== RUN TestEmailValid/tanpa-at
=== RUN TestEmailValid/domain-tanpa-titik
validasi_test.go:28: EmailValid("budi@localhost") = true; want false
--- FAIL: TestEmailValid (0.00s)
--- PASS: TestEmailValid/email-lengkap (0.00s)
--- PASS: TestEmailValid/lokal-kosong (0.00s)
--- PASS: TestEmailValid/tanpa-at (0.00s)
--- FAIL: TestEmailValid/domain-tanpa-titik (0.00s)
FAILTiga subtest tetap lulus dan hanya TestEmailValid/domain-tanpa-titik yang gagal, lengkap dengan baris validasi_test.go:28 — lokasi t.Errorf di dalam subtest tersebut. Fungsi induk ikut ditandai FAIL karena memuat subtest yang gagal, tetapi diagnosis sudah terisolasi pada satu subtest — tidak perlu membaca ulang seluruh isi fungsi induk.
t.Run("dengan-batas-panjang", func(t *testing.T) {
t.Cleanup(func() { /* pulihkan konfigurasi di sini */ })
// ... pemeriksaan subtest ...
})Subtest Paralel #
Setiap subtest juga dapat memanggil t.Parallel() agar berjalan serentak dengan subtest lain. Chapter ini sengaja tidak memakai t.Parallel() pada contoh utama karena go test -v mencetak hasil test paralel sesuai urutan penyelesaiannya, yang berubah-ubah antar eksekusi dan membuat output sulit dibandingkan. Paralelisme paling aman dipakai setelah test nyaman tanpa dirinya; pola tabel yang dikombinasikan dengan t.Run — satu subtest per baris tabel — menjadi fondasi yang rapi untuk itu.
Dengan subtest, struktur test kini presisi hingga level kasus. Chapter berikutnya mengangkat lapisan berikutnya — test helper, fungsi bantu yang menampung pemeriksaan berulang — dengan syarat baris pelaporan kegagalan tetap menunjuk test pemanggilnya, bukan helpernya.
Referensi #
- Dokumentasi testing.T.Run
— spesifikasi resmi subtest beserta aturan penamaan dan pencocokan
-run. - Dokumentasi package testing — referensi resmi seluruh API package testing.
- Testing flags pada perintah go
— aturan resmi flag
-rununtuk test maupun subtest.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
validasi_test.go
Unduh berkaspackage main
import "testing"
func TestEmailValid(t *testing.T) {
t.Run("email-lengkap", func(t *testing.T) {
for _, email := range []string{"[email protected]", "[email protected]"} {
if !EmailValid(email) {
t.Errorf("EmailValid(%q) = false; want true", email)
}
}
})
t.Run("lokal-kosong", func(t *testing.T) {
if EmailValid("@example.com") {
t.Errorf("EmailValid(%q) = true; want false", "@example.com")
}
})
t.Run("tanpa-at", func(t *testing.T) {
if EmailValid("budi.example.com") {
t.Errorf("EmailValid(%q) = true; want false", "budi.example.com")
}
})
t.Run("domain-tanpa-titik", func(t *testing.T) {
if EmailValid("budi@localhost") {
t.Errorf("EmailValid(%q) = true; want false", "budi@localhost")
}
})
}
validasi.go
Unduh berkaspackage main
import "strings"
// EmailValid memeriksa format email sederhana: tepat satu tanda @,
// bagian lokal tidak kosong, dan domain memuat titik serta tidak
// diawali atau diakhiri titik.
func EmailValid(email string) bool {
bagian := strings.Split(email, "@")
if len(bagian) != 2 {
return false
}
lokal, domain := bagian[0], bagian[1]
return lokal != "" &&
strings.Contains(domain, ".") &&
!strings.HasPrefix(domain, ".") &&
!strings.HasSuffix(domain, ".")
}