Test yang baik tidak hanya memeriksa kode, tetapi juga menyiapkan dunia tempat kode itu berjalan: direktori kerja, berkas masukan, data contoh. Kumpulan bahan uji tersebut disebut test fixture. Tanpa pola yang rapi, fixture berubah menjadi dua masalah klasik: berkas sisa menumpuk setiap kali test dijalankan, dan dua test yang berbagi berkas sama saling merusak hasil. Go menyelesaikan keduanya lewat t.TempDir, fstest.MapFS, dan pola fungsi setup. Pada chapter ini kita membangun fixture untuk katalog produk berbasis berkas, dari setup sampai pembersihan otomatis.

Kode Produksi: Katalog Berbasis Direktori #

Kode yang diuji adalah struct Katalog yang menyimpan daftar nama produk sebagai berkas JSON di dalam direktori, plus metode impor yang membaca daftar dari interface fs.FS:

go
package main

import (
	"bufio"
	"encoding/json"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"slices"
	"strings"
)

// Katalog menyimpan daftar nama produk sebagai berkas JSON di dalam
// sebuah direktori.
type Katalog struct {
	direktori string
}

// KatalogBaru menyiapkan katalog yang menyimpan datanya di dalam
// direktori tersebut.
func KatalogBaru(direktori string) (*Katalog, error) {
	if err := os.MkdirAll(direktori, 0o755); err != nil {
		return nil, fmt.Errorf("siapkan direktori: %w", err)
	}
	return &Katalog{direktori: direktori}, nil
}

// Simpan menyimpan daftar nama produk dengan kunci id.
func (k *Katalog) Simpan(id string, nama []string) error {
	berkas := filepath.Join(k.direktori, id+".json")
	data, err := json.Marshal(nama)
	if err != nil {
		return fmt.Errorf("marshal %s: %w", id, err)
	}
	if err := os.WriteFile(berkas, data, 0o644); err != nil {
		return fmt.Errorf("tulis %s: %w", berkas, err)
	}
	return nil
}

// Muat membaca kembali daftar nama produk dengan kunci id.
func (k *Katalog) Muat(id string) ([]string, error) {
	berkas := filepath.Join(k.direktori, id+".json")
	data, err := os.ReadFile(berkas)
	if err != nil {
		return nil, fmt.Errorf("baca %s: %w", berkas, err)
	}
	var nama []string
	if err := json.Unmarshal(data, &nama); err != nil {
		return nil, fmt.Errorf("unmarshal %s: %w", id, err)
	}
	return nama, nil
}

// DaftarID mengembalikan seluruh id yang tersimpan, terurut naik.
func (k *Katalog) DaftarID() ([]string, error) {
	entri, err := os.ReadDir(k.direktori)
	if err != nil {
		return nil, fmt.Errorf("baca direktori: %w", err)
	}
	var ids []string
	for _, e := range entri {
		if filepath.Ext(e.Name()) == ".json" {
			ids = append(ids, strings.TrimSuffix(e.Name(), ".json"))
		}
	}
	slices.Sort(ids)
	return ids, nil
}

// Impor membaca nama produk baris demi baris dari sebuah berkas di
// dalam fsys, menyimpannya dengan kunci id, lalu mengembalikan nama
// yang dibaca. fs.FS memungkinkan sumber berkas nyata, berkas hasil
// embed, maupun berkas palsu buatan fstest.MapFS.
func (k *Katalog) Impor(fsys fs.FS, id, namaBerkas string) ([]string, error) {
	f, err := fsys.Open(namaBerkas)
	if err != nil {
		return nil, fmt.Errorf("buka %s: %w", namaBerkas, err)
	}
	defer f.Close()

	var nama []string
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		if teks := scanner.Text(); teks != "" {
			nama = append(nama, teks)
		}
	}
	if err := scanner.Err(); err != nil {
		return nil, fmt.Errorf("baca %s: %w", namaBerkas, err)
	}
	if err := k.Simpan(id, nama); err != nil {
		return nil, err
	}
	return nama, nil
}

Tiga bagian penting bagi testing: KatalogBaru di baris 22-27 membuat direktori lewat os.MkdirAll sehingga test cukup menyerahkan path apa pun; Simpan dan Muat di baris 30-54 melakukan round-trip lewat berkas JSON; dan Impor di baris 76-97 menerima fs.FS, bukan path disk, sehingga sumber berkasnya bisa diganti — inilah pintu masuk fixture palsu.

Fixture sebagai Fungsi Setup #

Fixture pada berkas test memakai dua bentuk sekaligus: struct fixtureKatalog yang menghimpun bahan uji, dan fungsi fixtureKatalogBaru yang menyiapkan semuanya:

go
package main

import (
	"slices"
	"testing"
	"testing/fstest"
)

// fixtureKatalog menyiapkan seluruh bahan uji: katalog di direktori
// sementara beserta daftar contoh nama produk.
type fixtureKatalog struct {
	katalog *Katalog
	contoh  []string
}

// fixtureKatalogBaru bertindak sebagai setup: direktori sementara
// dibuat lewat t.TempDir dan katalog disiapkan di sana. Pembersihan
// (teardown) direktori berlangsung otomatis saat test selesai.
func fixtureKatalogBaru(t *testing.T) *fixtureKatalog {
	t.Helper()
	k, err := KatalogBaru(t.TempDir())
	if err != nil {
		t.Fatalf("siapkan fixture: %v", err)
	}
	return &fixtureKatalog{
		katalog: k,
		contoh:  []string{"Buku Tulis", "Pensil", "Penghapus"},
	}
}

func TestSimpanMuat(t *testing.T) {
	f := fixtureKatalogBaru(t)
	if err := f.katalog.Simpan("P001", f.contoh); err != nil {
		t.Fatalf("Simpan: %v", err)
	}
	got, err := f.katalog.Muat("P001")
	if err != nil {
		t.Fatalf("Muat: %v", err)
	}
	if !slices.Equal(got, f.contoh) {
		t.Errorf("got %v; want %v", got, f.contoh)
	}
}

func TestDaftarID(t *testing.T) {
	f := fixtureKatalogBaru(t)
	for _, id := range []string{"P002", "P001", "P003"} {
		if err := f.katalog.Simpan(id, f.contoh); err != nil {
			t.Fatalf("Simpan(%s): %v", id, err)
		}
	}
	got, err := f.katalog.DaftarID()
	if err != nil {
		t.Fatalf("DaftarID: %v", err)
	}
	want := []string{"P001", "P002", "P003"}
	if !slices.Equal(got, want) {
		t.Errorf("got %v; want %v", got, want)
	}
}

func TestImpor(t *testing.T) {
	f := fixtureKatalogBaru(t)
	fsys := fstest.MapFS{
		"P001.txt": &fstest.MapFile{Data: []byte("Buku Tulis\nPensil\nPenghapus\n")},
	}
	got, err := f.katalog.Impor(fsys, "P001", "P001.txt")
	if err != nil {
		t.Fatalf("Impor: %v", err)
	}
	if !slices.Equal(got, f.contoh) {
		t.Errorf("got %v; want %v", got, f.contoh)
	}
	disimpan, err := f.katalog.Muat("P001")
	if err != nil {
		t.Fatalf("Muat: %v", err)
	}
	if !slices.Equal(disimpan, f.contoh) {
		t.Errorf("disimpan %v; want %v", disimpan, f.contoh)
	}
}

func TestMuatIDHilang(t *testing.T) {
	f := fixtureKatalogBaru(t)
	_, err := f.katalog.Muat("TIDAK-ADA")
	if err == nil {
		t.Fatal("Muat id hilang: got nil; want error")
	}
}

Mari kita telaah baris demi baris berkas test di atas:

  1. Baris 11-14 (fixtureKatalog): struct fixture yang membungkus dua bahan uji: Katalog siap pakai dan daftar contoh berisi tiga nama produk.
  2. Baris 19 (func fixtureKatalogBaru(t *testing.T)): fungsi setup pembuat fixture. Konvensi fixtureXBaru (atau newX) menandakan fungsi yang selalu mengembalikan keadaan awal segar.
  3. Baris 20-21 (t.Helper() dan t.TempDir()): t.Helper menjaga nomor baris kegagalan tetap menunjuk test pemanggil; t.TempDir() membuat direktori sementara baru untuk test ini dan mendaftarkan penghapusannya otomatis — tanpa satu baris pembersihan pun di seluruh berkas test.
  4. Baris 23-24 (t.Fatalf): setup yang gagal menghentikan test langsung karena sisa test tidak bermakna tanpa fixture.
  5. Baris 25-28 (return &fixtureKatalog{...}): fixture terisi lengkap: katalog mengarah ke direktori sementara, contoh berisi data uji tetap.
  6. Baris 31-60 (TestSimpanMuat dan TestDaftarID): kedua test membuka barisnya dengan f := fixtureKatalogBaru(t) — masing-masing menerima fixture segar dengan direktori kosong berbeda, lalu memeriksa satu perilaku Katalog.
  7. Baris 62-81 (TestImpor): fixture berbasis fstest.MapFS — berkas P001.txt tidak pernah ada di disk karena isinya didefinisikan sebagai map. TestMuatIDHilang di baris 83-89 menutup daftar dengan jalur kegagalan.

Jalankan seluruh test dari direktori chapter:

bash
go test -v ./...

Output yang Dihasilkan:

text
=== RUN   TestSimpanMuat
--- PASS: TestSimpanMuat (0.00s)
=== RUN   TestDaftarID
--- PASS: TestDaftarID (0.00s)
=== RUN   TestImpor
--- PASS: TestImpor (0.00s)
=== RUN   TestMuatIDHilang
--- PASS: TestMuatIDHilang (0.00s)
PASS

Empat test lulus dan seluruh direktori sementara terhapus setelah selesai — jalankan berulang kali, tidak ada berkas sisa yang menumpuk.

t.TempDir: Direktori Unik per Test #

t.TempDir() mengembalikan path direktori baru setiap kali dipanggil. Setiap test memanggil fungsi setup-nya sendiri, sehingga TestSimpanMuat dan TestDaftarID bekerja pada direktori berbeda dan tidak saling melihat data lawan. Direktori tersebut dihapus otomatis ketika test pemiliknya selesai — termasuk saat test gagal di tengah jalan — lewat mekanisme pembersihan internal yang setara t.Cleanup.

Tanpa t.TempDir, pola yang umum adalah os.MkdirTemp("", ...) diikuti defer os.RemoveAll(...) — berfungsi, tetapi mudah lupa, dan satu lupa defer berarti berkas sisa menumpuk. t.TempDir memindahkan tanggung jawab tersebut ke kerangka test; pola manualnya dibahas pada chapter Temporary File .

Fixture Berbasis fs.FS dan fstest.MapFS #

Parameter fsys fs.FS pada Impor membuat sumber berkas dapat ditukar; test memakai fstest.MapFS — sistem berkas palsu berbentuk map, tanpa menyentuh disk. Baris 64-66 pada berkas test mendefinisikan satu berkas P001.txt berisi tiga baris nama, dan Impor membacanya seperti berkas sungguhan — lengkap dengan perilaku Open dan ErrNotExist untuk nama yang tidak ada.

Jenis fs.FS lain bekerja sama: os.DirFS("direktori") membungkus direktori nyata, dan embed.FS membungkus berkas yang ikut dikompilasi ke dalam program. Data uji yang stabil dan jarang berubah cocok ditempatkan lewat //go:embed lalu diuji lewat jalur fs.FS yang sama.

Peta Pola Fixture #

PolaBentukKapan dipakai
t.TempDir()Direktori unik per test, terhapus otomatisKode menulis dan membaca berkas nyata
fstest.MapFSSistem berkas palsu dari map, tanpa diskKode menerima fs.FS; data uji kecil dan sederhana
embed.FSBerkas ikut dikompilasi lewat //go:embedData uji besar, stabil, dipakai lintas test
Struct fixture + fungsi setupfixtureKatalog + fixtureKatalogBaru(t)Test butuh beberapa bahan sekaligus yang saling terkait

Fungsi setup menyiapkan fixture; t.TempDir dan t.Cleanup membersihkannya — keduanya dibahas pada chapter Test Helper .

Seluruh fixture sejauh ini berjalan di dalam proses test tanpa jaringan. Chapter berikutnya menambah dimensi terakhir pengujian tanpa dunia luar: httptest, paket untuk menguji handler HTTP tanpa menjalankan server sungguhan.

Referensi #

Berkas Sumber

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

katalog_test.go

Unduh berkas
go
package main

import (
	"slices"
	"testing"
	"testing/fstest"
)

// fixtureKatalog menyiapkan seluruh bahan uji: katalog di direktori
// sementara beserta daftar contoh nama produk.
type fixtureKatalog struct {
	katalog *Katalog
	contoh  []string
}

// fixtureKatalogBaru bertindak sebagai setup: direktori sementara
// dibuat lewat t.TempDir dan katalog disiapkan di sana. Pembersihan
// (teardown) direktori berlangsung otomatis saat test selesai.
func fixtureKatalogBaru(t *testing.T) *fixtureKatalog {
	t.Helper()
	k, err := KatalogBaru(t.TempDir())
	if err != nil {
		t.Fatalf("siapkan fixture: %v", err)
	}
	return &fixtureKatalog{
		katalog: k,
		contoh:  []string{"Buku Tulis", "Pensil", "Penghapus"},
	}
}

func TestSimpanMuat(t *testing.T) {
	f := fixtureKatalogBaru(t)
	if err := f.katalog.Simpan("P001", f.contoh); err != nil {
		t.Fatalf("Simpan: %v", err)
	}
	got, err := f.katalog.Muat("P001")
	if err != nil {
		t.Fatalf("Muat: %v", err)
	}
	if !slices.Equal(got, f.contoh) {
		t.Errorf("got %v; want %v", got, f.contoh)
	}
}

func TestDaftarID(t *testing.T) {
	f := fixtureKatalogBaru(t)
	for _, id := range []string{"P002", "P001", "P003"} {
		if err := f.katalog.Simpan(id, f.contoh); err != nil {
			t.Fatalf("Simpan(%s): %v", id, err)
		}
	}
	got, err := f.katalog.DaftarID()
	if err != nil {
		t.Fatalf("DaftarID: %v", err)
	}
	want := []string{"P001", "P002", "P003"}
	if !slices.Equal(got, want) {
		t.Errorf("got %v; want %v", got, want)
	}
}

func TestImpor(t *testing.T) {
	f := fixtureKatalogBaru(t)
	fsys := fstest.MapFS{
		"P001.txt": &fstest.MapFile{Data: []byte("Buku Tulis\nPensil\nPenghapus\n")},
	}
	got, err := f.katalog.Impor(fsys, "P001", "P001.txt")
	if err != nil {
		t.Fatalf("Impor: %v", err)
	}
	if !slices.Equal(got, f.contoh) {
		t.Errorf("got %v; want %v", got, f.contoh)
	}
	disimpan, err := f.katalog.Muat("P001")
	if err != nil {
		t.Fatalf("Muat: %v", err)
	}
	if !slices.Equal(disimpan, f.contoh) {
		t.Errorf("disimpan %v; want %v", disimpan, f.contoh)
	}
}

func TestMuatIDHilang(t *testing.T) {
	f := fixtureKatalogBaru(t)
	_, err := f.katalog.Muat("TIDAK-ADA")
	if err == nil {
		t.Fatal("Muat id hilang: got nil; want error")
	}
}

katalog.go

Unduh berkas
go
package main

import (
	"bufio"
	"encoding/json"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"slices"
	"strings"
)

// Katalog menyimpan daftar nama produk sebagai berkas JSON di dalam
// sebuah direktori.
type Katalog struct {
	direktori string
}

// KatalogBaru menyiapkan katalog yang menyimpan datanya di dalam
// direktori tersebut.
func KatalogBaru(direktori string) (*Katalog, error) {
	if err := os.MkdirAll(direktori, 0o755); err != nil {
		return nil, fmt.Errorf("siapkan direktori: %w", err)
	}
	return &Katalog{direktori: direktori}, nil
}

// Simpan menyimpan daftar nama produk dengan kunci id.
func (k *Katalog) Simpan(id string, nama []string) error {
	berkas := filepath.Join(k.direktori, id+".json")
	data, err := json.Marshal(nama)
	if err != nil {
		return fmt.Errorf("marshal %s: %w", id, err)
	}
	if err := os.WriteFile(berkas, data, 0o644); err != nil {
		return fmt.Errorf("tulis %s: %w", berkas, err)
	}
	return nil
}

// Muat membaca kembali daftar nama produk dengan kunci id.
func (k *Katalog) Muat(id string) ([]string, error) {
	berkas := filepath.Join(k.direktori, id+".json")
	data, err := os.ReadFile(berkas)
	if err != nil {
		return nil, fmt.Errorf("baca %s: %w", berkas, err)
	}
	var nama []string
	if err := json.Unmarshal(data, &nama); err != nil {
		return nil, fmt.Errorf("unmarshal %s: %w", id, err)
	}
	return nama, nil
}

// DaftarID mengembalikan seluruh id yang tersimpan, terurut naik.
func (k *Katalog) DaftarID() ([]string, error) {
	entri, err := os.ReadDir(k.direktori)
	if err != nil {
		return nil, fmt.Errorf("baca direktori: %w", err)
	}
	var ids []string
	for _, e := range entri {
		if filepath.Ext(e.Name()) == ".json" {
			ids = append(ids, strings.TrimSuffix(e.Name(), ".json"))
		}
	}
	slices.Sort(ids)
	return ids, nil
}

// Impor membaca nama produk baris demi baris dari sebuah berkas di
// dalam fsys, menyimpannya dengan kunci id, lalu mengembalikan nama
// yang dibaca. fs.FS memungkinkan sumber berkas nyata, berkas hasil
// embed, maupun berkas palsu buatan fstest.MapFS.
func (k *Katalog) Impor(fsys fs.FS, id, namaBerkas string) ([]string, error) {
	f, err := fsys.Open(namaBerkas)
	if err != nil {
		return nil, fmt.Errorf("buka %s: %w", namaBerkas, err)
	}
	defer f.Close()

	var nama []string
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		if teks := scanner.Text(); teks != "" {
			nama = append(nama, teks)
		}
	}
	if err := scanner.Err(); err != nil {
		return nil, fmt.Errorf("baca %s: %w", namaBerkas, err)
	}
	if err := k.Simpan(id, nama); err != nil {
		return nil, err
	}
	return nama, nil
}