Golang Dasar: Dari Nol hingga CLI Production-Ready9/10

Bab 9 dari 10

Testing, Table-Driven Tests, Coverage, dan Race Detector

Tujuan Pembelajaran

Setelah menyelesaikan bab ini, Anda mampu:

  • menulis unit test dengan package testing;
  • menyusun table-driven test dan subtest;
  • menguji file tanpa mencemari lingkungan pengguna;
  • membaca coverage secara kritis;
  • mendeteksi data race dengan race detector.

1. Dasar Pengujian Go

File pengujian berakhiran _test.go. Fungsi test bernama TestXxx dan menerima *testing.T.

Code
// task.go
package task

import (
	"errors"
	"strings"
)

type Task struct {
	ID    int
	Title string
}

func New(id int, title string) (Task, error) {
	title = strings.TrimSpace(title)
	if id < 1 {
		return Task{}, errors.New("ID harus positif")
	}
	if title == "" {
		return Task{}, errors.New("judul tidak boleh kosong")
	}
	return Task{ID: id, Title: title}, nil
}
Code
// task_test.go
package task

import "testing"

func TestNew(t *testing.T) {
	got, err := New(1, " Belajar test ")
	if err != nil {
		t.Fatalf("New() error = %v", err)
	}
	if got.Title != "Belajar test" {
		t.Errorf("Title = %q, ingin %q", got.Title, "Belajar test")
	}
}

Gunakan Fatalf jika test tidak dapat dilanjutkan; gunakan Errorf jika pemeriksaan lain masih bermakna. Pesan kegagalan sebaiknya memuat nilai aktual dan nilai yang diharapkan.

2. Table-Driven Tests

Table-driven test mengumpulkan beberapa skenario dalam satu struktur data. Setiap kasus dijalankan sebagai subtest dengan t.Run.

Code
package task

import "testing"

func TestNewTable(t *testing.T) {
	tests := []struct {
		name      string
		id        int
		title     string
		wantTitle string
		wantErr   bool
	}{
		{name: "valid", id: 1, title: " Belajar Go ", wantTitle: "Belajar Go"},
		{name: "ID nol", id: 0, title: "Belajar", wantErr: true},
		{name: "judul kosong", id: 1, title: "   ", wantErr: true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := New(tt.id, tt.title)
			if (err != nil) != tt.wantErr {
				t.Fatalf("New(%d, %q) error = %v, wantErr %t", tt.id, tt.title, err, tt.wantErr)
			}
			if !tt.wantErr && got.Title != tt.wantTitle {
				t.Errorf("Title = %q, ingin %q", got.Title, tt.wantTitle)
			}
		})
	}
}

Nama kasus harus menjelaskan perilaku, bukan nomor urut. Jangan memakai table-driven test jika hanya ada satu skenario sederhana dan tabel justru mengaburkan maksud.

3. Helper dan Cleanup

t.Helper() membuat lokasi kegagalan menunjuk ke pemanggil helper. t.Cleanup() memastikan pembersihan dijalankan setelah test.

Code
func writeFixture(t *testing.T, dir, content string) string {
	t.Helper()
	path := filepath.Join(dir, "tasks.json")
	if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
		t.Fatalf("tulis fixture: %v", err)
	}
	return path
}

Untuk file, gunakan t.TempDir(). Direktori unik tersebut dibersihkan otomatis dan tidak menyentuh data pengguna.

Code
func TestLoadMissingFile(t *testing.T) {
	path := filepath.Join(t.TempDir(), "missing.json")
	got, err := Load(path)
	if err != nil {
		t.Fatalf("Load() error = %v", err)
	}
	if len(got) != 0 {
		t.Fatalf("len(Load()) = %d, ingin 0", len(got))
	}
}

4. Determinisme dan Isolasi

Test yang baik memberikan hasil sama pada setiap eksekusi. Hindari ketergantungan pada:

  • waktu nyata tanpa cara mengendalikannya;
  • urutan iterasi map;
  • jaringan eksternal;
  • direktori kerja atau file pengguna;
  • state global yang dibagi antartest.

Jika logika membutuhkan waktu, injeksikan fungsi atau nilai waktu. Jika keluaran berasal dari map, urutkan key sebelum membandingkan. Jalankan t.Parallel() hanya setelah memastikan tidak ada state bersama yang berubah.

Memuat diagram…

5. Membandingkan Error

Jangan membandingkan teks error jika kode menyediakan sentinel error atau tipe khusus.

Code
var ErrNotFound = errors.New("tugas tidak ditemukan")

if !errors.Is(err, ErrNotFound) {
	t.Fatalf("error = %v, ingin ErrNotFound", err)
}

Gunakan errors.As untuk memeriksa tipe error. Pemeriksaan string rapuh karena perubahan redaksi dapat merusak test tanpa mengubah perilaku.

6. Coverage

Coverage menunjukkan bagian statement yang dieksekusi, bukan kualitas pengujian.

Code
go test ./...
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
go tool cover -html=coverage.out

Coverage tinggi tetap dapat melewatkan assertion penting, error path, race, atau kebutuhan bisnis. Gunakan laporan untuk menemukan area yang belum diuji, bukan sebagai satu-satunya target kualitas.

7. Race Detector

Data race terjadi ketika goroutine mengakses lokasi memori yang sama secara bersamaan, setidaknya satu akses adalah penulisan, dan tidak ada sinkronisasi yang benar.

Code
package counter

import "sync"

type Counter struct {
	mu sync.Mutex
	n  int
}

func (c *Counter) Add() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.n++
}

func (c *Counter) Value() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.n
}
Code
package counter

import (
	"sync"
	"testing"
)

func TestCounterConcurrent(t *testing.T) {
	var c Counter
	var wg sync.WaitGroup
	for range 100 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			c.Add()
		}()
	}
	wg.Wait()
	if got := c.Value(); got != 100 {
		t.Fatalf("Value() = %d, ingin 100", got)
	}
}
Code
go test -race ./...

Race detector hanya menemukan race pada jalur yang benar-benar dijalankan. Ia menambah waktu dan memori, tetapi sangat bernilai pada CI atau sebelum rilis. Race detector bukan pengganti desain sinkronisasi dan tidak mendeteksi semua logical race.

8. Perintah Verifikasi Praktis

Code
gofmt -w .
go vet ./...
go test ./...
go test -shuffle=on -count=10 ./...
go test -race ./...
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out

-shuffle=on membantu menemukan ketergantungan urutan. -count=10 membantu mengungkap test yang sesekali gagal, tetapi bukan bukti mutlak bahwa test bebas flakiness.

Kesalahan Umum

  1. Hanya menguji happy path. Sertakan input batas, error, dan data rusak.
  2. Assertion tanpa pesan diagnostik. Tampilkan aktual, harapan, dan input relevan.
  3. Test memakai file pengguna. Gunakan t.TempDir().
  4. Mengejar 100% coverage tanpa nilai. Prioritaskan risiko dan perilaku penting.
  5. Memakai t.Parallel() pada state bersama. Isolasi atau sinkronkan terlebih dahulu.
  6. Mengabaikan hasil -race. Race adalah bug; perbaiki, jangan sekadar menonaktifkan pemeriksaan.
  7. Sleep untuk sinkronisasi. Gunakan channel, WaitGroup, atau primitive sinkronisasi lain.

Latihan

  1. Tambahkan kasus batas pada TestNewTable, termasuk whitespace Unicode.
  2. Tulis test Load untuk file valid, tidak ada, kosong, dan JSON rusak menggunakan t.TempDir().
  3. Buat helper fixture dengan t.Helper().
  4. Sengaja hapus mutex dari Counter, jalankan go test -race, lalu perbaiki kembali.
  5. Buat laporan coverage dan identifikasi satu error path yang belum diuji.

Ringkasan

  • Package testing menyediakan unit test tanpa framework tambahan.
  • Table-driven tests efektif untuk banyak variasi perilaku yang sama.
  • Helper, t.TempDir, dan input terkendali menjaga test diagnostik serta deterministik.
  • Coverage mengukur eksekusi statement, bukan kebenaran.
  • Race detector menemukan akses memori tidak tersinkronisasi pada jalur yang diuji.
Testing, Table-Driven Tests, Coverage, dan Race Detector · Golang Dasar: Dari Nol hingga CLI Production-Ready