Chapter 9 of 10
Setelah menyelesaikan bab ini, Anda mampu:
testing;File pengujian berakhiran _test.go. Fungsi test bernama TestXxx dan menerima *testing.T.
// 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
}
// 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.
Table-driven test mengumpulkan beberapa skenario dalam satu struktur data. Setiap kasus dijalankan sebagai subtest dengan t.Run.
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.
t.Helper() membuat lokasi kegagalan menunjuk ke pemanggil helper. t.Cleanup() memastikan pembersihan dijalankan setelah test.
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.
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))
}
}
Test yang baik memberikan hasil sama pada setiap eksekusi. Hindari ketergantungan pada:
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.
Jangan membandingkan teks error jika kode menyediakan sentinel error atau tipe khusus.
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.
Coverage menunjukkan bagian statement yang dieksekusi, bukan kualitas pengujian.
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.
Data race terjadi ketika goroutine mengakses lokasi memori yang sama secara bersamaan, setidaknya satu akses adalah penulisan, dan tidak ada sinkronisasi yang benar.
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
}
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)
}
}
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.
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.
t.TempDir().t.Parallel() pada state bersama. Isolasi atau sinkronkan terlebih dahulu.-race. Race adalah bug; perbaiki, jangan sekadar menonaktifkan pemeriksaan.WaitGroup, atau primitive sinkronisasi lain.TestNewTable, termasuk whitespace Unicode.Load untuk file valid, tidak ada, kosong, dan JSON rusak menggunakan t.TempDir().t.Helper().Counter, jalankan go test -race, lalu perbaiki kembali.testing menyediakan unit test tanpa framework tambahan.t.TempDir, dan input terkendali menjaga test diagnostik serta deterministik.