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:

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

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

  1. Baris 5 (func TestEmailValid(t *testing.T)): fungsi test induk. go test menjalankan fungsi ini; subtest dijalankan dari dalam fungsi induk secara berurutan.
  2. 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.
  3. Baris 7-11 (loop di dalam subtest): subtest boleh berisi apa pun yang biasa ditulis test, termasuk loop dan beberapa t.Errorf. Subtest email-lengkap memeriksa dua alamat valid sekaligus karena keduanya menguji aturan yang sama.
  4. Baris 14-18 (subtest lokal-kosong): memeriksa satu kasus gagal: bagian lokal kosong. t.Errorf yang dipanggil di dalam closure ini akan dilaporkan atas nama subtest, bukan atas nama fungsi induk.
  5. Baris 20-30 (tanpa-at dan domain-tanpa-titik): dua subtest terakhir yang masing-masing menguji satu aturan tersisa. Setiap subtest menerima parameter t *testing.T sendiri, sehingga kegagalan pada satu subtest tidak menghentikan subtest berikutnya.

Jalankan seluruh test dari direktori chapter:

bash
go test -v ./...

Output yang Dihasilkan:

text
=== 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)
PASS

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

bash
go test -v -run "TestEmailValid/tanpa-at" ./...

Output yang Dihasilkan:

text
=== RUN   TestEmailValid
=== RUN   TestEmailValid/tanpa-at
--- PASS: TestEmailValid (0.00s)
    --- PASS: TestEmailValid/tanpa-at (0.00s)
PASS

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

text
=== 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)
FAIL

Tiga 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.

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

Berkas Sumber

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

validasi_test.go

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

validasi.go

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