← Semua pembelajaran / Go Nol → Enterprise
Fase 8 · Testing & Kualitas Kode

Testing di Go — pustaka standar sudah cukup

Tes di Go adalah fungsi Go biasa. Tidak ada anotasi, tidak ada runner terpisah, tidak ada bahasa assertion yang harus dipelajari — dan itu membuat tes terbaca oleh siapa pun yang bisa membaca kodenya.

Sumber asli pkg.go.dev Resmi Rangkuman ~8 menit baca

Intisari

  • Berkas *_test.go, fungsi func TestXxx(t *testing.T). Tidak ikut ke binari produksi.
  • t.Run membuat subtes yang bisa dijalankan sendiri dan dilaporkan terpisah.
  • t.Cleanup lebih baik daripada defer — ia jalan setelah subtes selesai, dan bisa dipasang dari fungsi bantu.
  • t.Helper() membuat kegagalan menunjuk baris pemanggil, bukan baris di dalam fungsi bantu.
  • t.Parallel() mempercepat suite dan mengungkap ketergantungan tersembunyi antar tes.

Bentuk dasar

// internal/produk/produk_test.go
package produk

import "testing"

func TestAmbil(t *testing.T) {
	p := Produk{Stok: 10}

	if err := p.Ambil(3); err != nil {
		t.Fatalf("Ambil(3) error = %v, mau nil", err)
	}
	if p.Stok != 7 {
		t.Errorf("Stok = %d, mau 7", p.Stok)
	}
}
MethodEfekKapan
t.ErrorfMenandai gagal, lanjutBeberapa pemeriksaan independen
t.FatalfMenandai gagal, berhentiPemeriksaan berikutnya akan panic tanpa ini
t.SkipLewatiButuh sumber daya yang tidak ada
t.CleanupJalankan saat tes selesaiBersihkan berkas, koneksi, data
t.HelperKegagalan menunjuk pemanggilDi setiap fungsi bantu tes
t.TempDirFolder sementara, dibersihkan otomatisTes yang menyentuh berkas
t.SetenvSet environment, dikembalikan otomatisTes konfigurasi (tidak bisa paralel)
t.ContextContext yang dibatalkan saat tes selesai (Go 1.24+)Apa pun yang menerima context

Format pesan gagal yang dipakai seluruh ekosistem Go: t.Errorf("Fungsi(input) = hasil, mau harapan"). Ia menyebut apa yang dipanggil, apa yang didapat, dan apa yang diharapkan — dalam satu baris. Assertion seperti assert.Equal(t, 7, p.Stok) menghasilkan pesan yang tidak menyebutkan apa yang sedang diuji.

Kenapa Go tidak punya assert

// Gaya pustaka standar — verbose, tapi pesannya persis
if got := Hitung(3); got != 9 {
	t.Errorf("Hitung(3) = %d, mau 9", got)
}

// Untuk struct dan slice, pakai perbandingan mendalam
if diff := cmp.Diff(mau, got); diff != "" {
	t.Errorf("Cari() selisih (-mau +dapat):\n%s", diff)
}

github.com/google/go-cmp adalah satu-satunya dependensi tes yang benar-benar layak. reflect.DeepEqual memberi tahu "tidak sama" tanpa menjelaskan apa; cmp.Diff menunjukkan field mana yang berbeda. Ia juga bisa mengabaikan field tertentu — berguna untuk ID dan CreatedAt yang dibuat database.

cmp.Diff(mau, got, cmpopts.IgnoreFields(Produk{}, "ID", "Dibuat"))

t.Cleanup mengalahkan defer

// Fungsi bantu bisa mendaftarkan pembersihannya sendiri — sesuatu
// yang tidak bisa dilakukan defer, karena defer terikat ke fungsi ini.
func dbUji(t *testing.T) *sql.DB {
	t.Helper()

	db, err := sql.Open("pgx", dsnUji)
	if err != nil {
		t.Fatalf("buka db uji: %v", err)
	}
	t.Cleanup(func() { db.Close() })

	skema := namaSkemaAcak()
	if _, err := db.Exec("CREATE SCHEMA " + skema); err != nil {
		t.Fatalf("buat skema: %v", err)
	}
	t.Cleanup(func() { db.Exec("DROP SCHEMA " + skema + " CASCADE") })

	return db
}

func TestRepo(t *testing.T) {
	db := dbUji(t)      // satu baris; pembersihan sudah terurus
	...
}

Subtes dan paralel

func TestValidasi(t *testing.T) {
	t.Parallel()      // tes ini paralel terhadap tes lain

	kasus := map[string]struct {
		masuk string
		mau   bool
	}{
		"kosong":    {"", false},
		"terlalu pendek": {"ab", false},
		"benar":     {"kopi arabika", true},
	}

	for nama, k := range kasus {
		t.Run(nama, func(t *testing.T) {
			t.Parallel()          // subtes paralel terhadap sesamanya
			if got := Valid(k.masuk); got != k.mau {
				t.Errorf("Valid(%q) = %v, mau %v", k.masuk, got, k.mau)
			}
		})
	}
}
go test ./...
go test -run 'TestValidasi/benar' ./internal/produk   # satu subtes saja
go test -v ./...                                       # tampilkan yang lulus juga
go test -count=1 ./...                                 # abaikan cache
go test -race ./...                                    # WAJIB di CI (Fase 2)
go test -shuffle=on ./...                              # acak urutan tes

-shuffle=on menemukan tes yang diam-diam saling bergantung — misalnya tes B yang hanya lulus karena tes A sudah mengisi data. Ketergantungan seperti itu berubah jadi kegagalan misterius begitu ada yang menambah tes baru di tengah. Jalankan sesekali di CI.

Cakupan tes

go test -cover ./...
go test -coverprofile=cover.out ./...
go tool cover -html=cover.out           # peta berwarna per baris
go tool cover -func=cover.out | tail -1

Cakupan tinggi tidak membuktikan apa pun. Tes yang memanggil setiap fungsi tanpa memeriksa hasilnya menghasilkan 100% cakupan dan nol jaminan. Yang jauh lebih berguna adalah daftar pendek ini, dan memastikan semuanya punya tes:

  1. Setiap aturan otorisasi — dari kedua sisi (Fase 6).
  2. Setiap alur yang menyentuh uang atau data pribadi.
  3. Bentuk respons setiap endpoint API publik.
  4. Setiap bug yang pernah terjadi — tesnya ditulis sebelum perbaikannya.
  5. Perilaku pada kegagalan: database mati, API luar timeout, context dibatalkan.

Paket tes: internal atau eksternal

package produk        // bisa menguji fungsi tak diekspor

package produk_test   // hanya API publik — memaksa tes lewat pintu yang sama
                      // dengan pemakai sungguhan

Keduanya boleh ada di folder yang sama. Aturan praktis: pakai _test secara bawaan, karena ia menguji kontrak yang benar-benar kamu janjikan; turun ke paket internal hanya untuk algoritma rumit yang memang perlu diuji dari dalam.

Data uji dan golden file

// Berkas di testdata/ diabaikan alat Go — tempat resminya data uji.
func TestRender(t *testing.T) {
	got := Render(dataContoh)

	emas := filepath.Join("testdata", "beranda.golden.html")
	if *perbarui {                     // go test -update
		os.WriteFile(emas, got, 0o644)
	}

	mau, err := os.ReadFile(emas)
	if err != nil {
		t.Fatal(err)
	}
	if diff := cmp.Diff(string(mau), string(got)); diff != "" {
		t.Errorf("keluaran berubah (-emas +baru):\n%s", diff)
	}
}

Latihan: tulis tes untuk fungsi domainmu dengan format pesan "Fungsi(x) = y, mau z". Tambahkan fungsi bantu yang memanggil t.Helper() dan buktikan nomor baris kegagalannya menunjuk ke tesmu, bukan ke fungsi bantu. Lalu hapus t.Helper() dan bandingkan.

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