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.
Intisari
- Berkas
*_test.go, fungsifunc TestXxx(t *testing.T). Tidak ikut ke binari produksi. t.Runmembuat subtes yang bisa dijalankan sendiri dan dilaporkan terpisah.t.Cleanuplebih baik daripadadefer— 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)
}
}
| Method | Efek | Kapan |
|---|---|---|
t.Errorf | Menandai gagal, lanjut | Beberapa pemeriksaan independen |
t.Fatalf | Menandai gagal, berhenti | Pemeriksaan berikutnya akan panic tanpa ini |
t.Skip | Lewati | Butuh sumber daya yang tidak ada |
t.Cleanup | Jalankan saat tes selesai | Bersihkan berkas, koneksi, data |
t.Helper | Kegagalan menunjuk pemanggil | Di setiap fungsi bantu tes |
t.TempDir | Folder sementara, dibersihkan otomatis | Tes yang menyentuh berkas |
t.Setenv | Set environment, dikembalikan otomatis | Tes konfigurasi (tidak bisa paralel) |
t.Context | Context 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:
- Setiap aturan otorisasi — dari kedua sisi (Fase 6).
- Setiap alur yang menyentuh uang atau data pribadi.
- Bentuk respons setiap endpoint API publik.
- Setiap bug yang pernah terjadi — tesnya ditulis sebelum perbaikannya.
- 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.