โ† Semua pembelajaran / Go Nol โ†’ Enterprise
Fase 8 ยท Testing & Kualitas Kode

Tes tabel & test double

Menambah kasus uji seharusnya berarti menambah satu baris, bukan menyalin satu fungsi. Tes tabel memberi itu, dan interface implisit Go membuat test double jadi struct kecil belasan baris.

Sumber asli go.dev Resmi Rangkuman ~7 menit baca

Intisari

  • Satu tes, banyak kasus dalam satu slice atau map โ€” menambah kasus berarti menambah satu baris.
  • Beri nama tiap kasus; nama itu muncul di keluaran kegagalan dan bisa dipakai -run.
  • Fake (implementasi sederhana) hampir selalu lebih baik daripada mock (harapan panggilan).
  • Karena interface Go implisit, test double cuma struct dengan method yang dibutuhkan โ€” tanpa pustaka.
  • Jangan menguji interaksi ("method X dipanggil dua kali") kecuali interaksinya memang bagian dari kontrak.

Bentuk tes tabel

func TestHitungOngkir(t *testing.T) {
	t.Parallel()

	kasus := []struct {
		nama    string
		berat   int
		zona    string
		mau     int64
		mauErr  error
	}{
		{"dalam kota ringan", 500, "kota", 9000, nil},
		{"dalam kota berat", 5000, "kota", 25000, nil},
		{"luar pulau", 1000, "luar-pulau", 45000, nil},
		{"berat nol", 0, "kota", 0, ErrBeratTidakValid},
		{"zona tak dikenal", 1000, "bulan", 0, ErrZonaTidakDikenal},
	}

	for _, k := range kasus {
		t.Run(k.nama, func(t *testing.T) {
			t.Parallel()

			got, err := HitungOngkir(k.berat, k.zona)

			if !errors.Is(err, k.mauErr) {
				t.Fatalf("HitungOngkir(%d, %q) err = %v, mau %v",
					k.berat, k.zona, err, k.mauErr)
			}
			if got != k.mau {
				t.Errorf("HitungOngkir(%d, %q) = %d, mau %d",
					k.berat, k.zona, got, k.mau)
			}
		})
	}
}
--- FAIL: TestHitungOngkir/luar_pulau (0.00s)
    ongkir_test.go:31: HitungOngkir(1000, "luar-pulau") = 40000, mau 45000

Yang membuat pola ini menang: kasus baru adalah satu baris data, bukan satu fungsi baru. Itu menurunkan biaya menambah kasus tepi sampai hampir nol โ€” dan kasus tepi justru yang paling sering menyimpan bug.

Kapan tes tabel bukan pilihan

// โŒ Kalau setiap kasus butuh persiapan berbeda, tabelnya penuh
//    field kondisional dan lebih sulit dibaca daripada tes terpisah.
kasus := []struct {
	nama          string
	siapkanDB     func(*testing.T, *sql.DB)
	tiruanGagal   bool
	lewatiJika    string
	...
}

Kalau tabelmu punya lebih dari sekitar enam kolom, atau berisi banyak func, tulis beberapa tes terpisah dengan nama yang jelas. Tes tabel untuk kasus yang seragam.

Test double tanpa pustaka

// Interface yang dideklarasikan domain (Fase 1)
type Penyimpan interface {
	Ambil(ctx context.Context, id int64) (Produk, error)
	Simpan(ctx context.Context, p Produk) error
}

// FAKE: implementasi sederhana yang benar-benar bekerja.
type penyimpanMemori struct {
	mu   sync.Mutex
	data map[int64]Produk
	errAmbil error         // untuk menguji jalur kegagalan
}

func penyimpanBaru(isi ...Produk) *penyimpanMemori {
	m := &penyimpanMemori{data: map[int64]Produk{}}
	for _, p := range isi {
		m.data[p.ID] = p
	}
	return m
}

func (m *penyimpanMemori) Ambil(_ context.Context, id int64) (Produk, error) {
	if m.errAmbil != nil {
		return Produk{}, m.errAmbil
	}
	m.mu.Lock()
	defer m.mu.Unlock()

	p, ok := m.data[id]
	if !ok {
		return Produk{}, ErrTidakDitemukan
	}
	return p, nil
}

func (m *penyimpanMemori) Simpan(_ context.Context, p Produk) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.data[p.ID] = p
	return nil
}
func TestBeli(t *testing.T) {
	simpan := penyimpanBaru(Produk{ID: 1, Stok: 10})
	svc := NewLayanan(simpan, cacheKosong{}, slog.Default())

	if err := svc.Beli(t.Context(), 1, 3); err != nil {
		t.Fatalf("Beli() error = %v", err)
	}

	p, _ := simpan.Ambil(t.Context(), 1)
	if p.Stok != 7 {
		t.Errorf("stok setelah beli = %d, mau 7", p.Stok)
	}
}

func TestBeliSaatDBGagal(t *testing.T) {
	simpan := penyimpanBaru()
	simpan.errAmbil = errors.New("koneksi putus")
	svc := NewLayanan(simpan, cacheKosong{}, slog.Default())

	if err := svc.Beli(t.Context(), 1, 3); err == nil {
		t.Fatal("mau error, dapat nil")
	}
}

Perhatikan bahwa tidak ada satu pun pustaka mocking di atas. Karena interface Go implisit, struct mana pun yang punya method yang tepat sudah memenuhinya โ€” dan fake berbasis map hampir selalu lebih mudah dibaca serta lebih tahan terhadap perubahan daripada mock yang menyatakan urutan panggilan.

Fake versus mock

FakeMock
IsiImplementasi sederhana yang bekerjaHarapan: "method X dipanggil dengan Y"
MengujiHasilInteraksi
Saat implementasi diubahTetap lulus kalau perilakunya samaSering ikut gagal
KeterbacaanKode Go biasaDSL pustaka
Cocok untukHampir semuaEfek samping yang memang jadi kontrak (email terkirim, pesan diantre)
// Kalau yang PENTING memang interaksinya, catat saja โ€” tetap tanpa pustaka.
type pengirimPalsu struct {
	terkirim []Email
}

func (p *pengirimPalsu) Kirim(_ context.Context, e Email) error {
	p.terkirim = append(p.terkirim, e)
	return nil
}

// lalu di tes:
if len(pengirim.terkirim) != 1 {
	t.Fatalf("email terkirim = %d, mau 1", len(pengirim.terkirim))
}
if pengirim.terkirim[0].Ke != "[email protected]" {
	t.Errorf("penerima = %q", pengirim.terkirim[0].Ke)
}

Menguji waktu

// Waktu disuntikkan sebagai dependensi (Fase 5)
svc := NewLayanan(simpan, katalog,
	DenganJam(func() time.Time {
		return time.Date(2026, 3, 1, 10, 0, 0, 0, time.UTC)
	}))

// Sekarang "kupon kedaluwarsa setelah 30 hari" bisa diuji
// tanpa satu pun time.Sleep.

Setiap time.Sleep di dalam tes adalah utang. Ia memperlambat suite, dan cepat atau lambat ia jadi flaky di CI yang lebih lambat daripada laptopmu. Untuk kode berbasis waktu: suntikkan jam. Untuk concurrency berbasis waktu: testing/synctest (materi berikutnya).

Latihan: ubah salah satu tesmu jadi tes tabel dengan minimal lima kasus, termasuk dua kasus error. Lalu tulis fake berbasis map untuk repository-mu dan uji satu alur service tanpa menyentuh database. Terakhir, tambahkan field errAmbil pada fake itu dan tulis tes untuk jalur kegagalannya โ€” jalur itu biasanya yang paling jarang diuji dan paling sering rusak.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.