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:
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:
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:
- Baris 11-14 (
fixtureKatalog): struct fixture yang membungkus dua bahan uji:Katalogsiap pakai dan daftarcontohberisi tiga nama produk. - Baris 19 (
func fixtureKatalogBaru(t *testing.T)): fungsi setup pembuat fixture. KonvensifixtureXBaru(ataunewX) menandakan fungsi yang selalu mengembalikan keadaan awal segar. - Baris 20-21 (
t.Helper()dant.TempDir()):t.Helpermenjaga 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. - Baris 23-24 (
t.Fatalf): setup yang gagal menghentikan test langsung karena sisa test tidak bermakna tanpa fixture. - Baris 25-28 (
return &fixtureKatalog{...}): fixture terisi lengkap: katalog mengarah ke direktori sementara,contohberisi data uji tetap. - Baris 31-60 (
TestSimpanMuatdanTestDaftarID): kedua test membuka barisnya denganf := fixtureKatalogBaru(t)— masing-masing menerima fixture segar dengan direktori kosong berbeda, lalu memeriksa satu perilakuKatalog. - Baris 62-81 (
TestImpor): fixture berbasisfstest.MapFS— berkasP001.txttidak pernah ada di disk karena isinya didefinisikan sebagai map.TestMuatIDHilangdi baris 83-89 menutup daftar dengan jalur kegagalan.
Jalankan seluruh test dari direktori chapter:
go test -v ./...Output yang Dihasilkan:
=== RUN TestSimpanMuat
--- PASS: TestSimpanMuat (0.00s)
=== RUN TestDaftarID
--- PASS: TestDaftarID (0.00s)
=== RUN TestImpor
--- PASS: TestImpor (0.00s)
=== RUN TestMuatIDHilang
--- PASS: TestMuatIDHilang (0.00s)
PASSEmpat 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 #
| Pola | Bentuk | Kapan dipakai |
|---|---|---|
t.TempDir() | Direktori unik per test, terhapus otomatis | Kode menulis dan membaca berkas nyata |
fstest.MapFS | Sistem berkas palsu dari map, tanpa disk | Kode menerima fs.FS; data uji kecil dan sederhana |
embed.FS | Berkas ikut dikompilasi lewat //go:embed | Data uji besar, stabil, dipakai lintas test |
| Struct fixture + fungsi setup | fixtureKatalog + 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 #
- Dokumentasi testing.T.TempDir — spesifikasi resmi direktori sementara per test.
- Dokumentasi package fstest
— referensi resmi
fstest.MapFSdanfstest.MapFile. - Dokumentasi package testing — referensi resmi seluruh API kerangka test.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
katalog_test.go
Unduh berkaspackage 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 berkaspackage 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
}