Testing #
Testing adalah warga kelas satu di Go — bukan sesuatu yang ditambahkan belakangan, melainkan bagian inti dari toolchain sejak awal. Package testing menyediakan framework yang bersih dan minimalis: cukup buat file _test.go, tulis fungsi TestXxx, dan jalankan go test. Tidak ada library assertion yang wajib, tidak ada test runner eksternal, tidak ada konfigurasi rumit. Filosofi ini mendorong test yang sederhana dan eksplisit. Di luar unit test biasa, Go juga mendukung benchmark dengan BenchmarkXxx untuk mengukur performa, sub-test dengan t.Run untuk organisasi yang lebih baik, dan fuzzing dengan FuzzXxx untuk menemukan edge case secara otomatis. Memahami testing di Go dengan baik adalah investasi yang langsung terasa manfaatnya — kode yang mudah di-test biasanya juga kode yang baik desainnya.
Gambaran Besar Package testing #
flowchart TD
T["package testing"] --> Unit["Unit Test\nTestXxx(t *testing.T)"]
T --> Bench["Benchmark\nBenchmarkXxx(b *testing.B)"]
T --> Fuzz["Fuzzing\nFuzzXxx(f *testing.F)"]
T --> Example["Example\nExampleXxx()"]
Unit --> TA["t.Error / t.Errorf\nlanjutkan test meski gagal"]
Unit --> TB["t.Fatal / t.Fatalf\nhentikan test segera"]
Unit --> TC["t.Run\nsub-test"]
Unit --> TD["t.Helper\nmark sebagai helper"]
Unit --> TE["t.Cleanup\njalankan setelah test selesai"]
Unit --> TF["t.Parallel\njalankan paralel"]
Unit --> TG["t.Skip / t.Skipf\nskip test"]
Unit --> TH["t.TempDir\ndirektori temporary"]
Bench --> BA["b.N — jumlah iterasi"]
Bench --> BB["b.ResetTimer\nreset timer setelah setup"]
Bench --> BC["b.ReportAllocs\nlaporkan alokasi"]
Bench --> BD["b.RunParallel\nbenchmark paralel"]
style T fill:#4f86c6,color:#fff
style Unit fill:#e8f5e9
style Bench fill:#e3f2fd
style Fuzz fill:#fff3e0
style Example fill:#f3e5f5Unit Test Dasar #
File test di Go harus berakhiran _test.go dan berada di package yang sama (atau package _test untuk black-box testing). Nama fungsi test harus dimulai dengan Test diikuti huruf kapital:
// Struktur direktori
// myapp/
// ├── hitung.go
// └── hitung_test.go
// hitung.go
package hitung
func Tambah(a, b int) int {
return a + b
}
func Bagi(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("tidak bisa dibagi nol")
}
return a / b, nil
}
func IsPalindrome(s string) bool {
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
if runes[i] != runes[j] {
return false
}
}
return true
}
// hitung_test.go
package hitung
import (
"testing"
)
func TestTambah(t *testing.T) {
hasil := Tambah(2, 3)
if hasil != 5 {
t.Errorf("Tambah(2, 3) = %d, ingin 5", hasil)
}
}
func TestBagi(t *testing.T) {
// Test kasus normal
hasil, err := Bagi(10, 2)
if err != nil {
t.Fatalf("Bagi(10, 2) mengembalikan error yang tidak diharapkan: %v", err)
}
if hasil != 5.0 {
t.Errorf("Bagi(10, 2) = %f, ingin 5.0", hasil)
}
// Test pembagian nol
_, err = Bagi(10, 0)
if err == nil {
t.Error("Bagi(10, 0) seharusnya mengembalikan error")
}
}
t.Error vs t.Fatal #
func TestPerbedaanErrorFatal(t *testing.T) {
// t.Error — catat kegagalan tapi LANJUTKAN test
// Gunakan saat kamu ingin melihat semua kegagalan sekaligus
hasil := Tambah(1, 1)
if hasil != 2 {
t.Errorf("Tambah(1, 1) = %d, ingin 2", hasil)
// Test berlanjut setelah ini
}
// t.Fatal — catat kegagalan dan HENTIKAN test segera
// Gunakan saat langkah berikutnya tidak masuk akal jika langkah ini gagal
conn, err := bukaKoneksi()
if err != nil {
t.Fatalf("gagal buka koneksi: %v", err)
// Test berhenti di sini — kode setelah ini tidak dieksekusi
}
defer conn.Tutup()
// Ini tidak akan dieksekusi jika t.Fatal dipanggil
conn.Kirim("ping")
}
Table-Driven Test — Pola Paling Idiomatis #
Table-driven test adalah pola yang paling dianjurkan di Go — mendefinisikan semua kasus uji dalam satu tabel, lalu mengiterasi dan menjalankan masing-masing:
flowchart LR
subgraph Table["Test Table ([]struct{...})"]
TC1["Kasus 1\ninput: 2,3\nexpect: 5"]
TC2["Kasus 2\ninput: -1,1\nexpect: 0"]
TC3["Kasus 3\ninput: 0,0\nexpect: 0"]
TC4["Kasus 4\n(edge case)\ninput: MaxInt,1\nexpect: error"]
end
subgraph Loop["for _, tc := range tests"]
Run["t.Run(tc.nama, func(t))"]
end
subgraph Result["Hasil"]
R1["PASS: Kasus 1"]
R2["PASS: Kasus 2"]
R3["PASS: Kasus 3"]
R4["FAIL: Kasus 4 — detail error"]
end
Table --> Loop --> Resultfunc TestTambahTableDriven(t *testing.T) {
tests := []struct {
nama string
a, b int
ingin int
}{
{"positif + positif", 2, 3, 5},
{"negatif + positif", -1, 1, 0},
{"nol + nol", 0, 0, 0},
{"besar + besar", 1000000, 2000000, 3000000},
{"negatif + negatif", -5, -3, -8},
}
for _, tc := range tests {
t.Run(tc.nama, func(t *testing.T) {
hasil := Tambah(tc.a, tc.b)
if hasil != tc.ingin {
t.Errorf("Tambah(%d, %d) = %d, ingin %d",
tc.a, tc.b, hasil, tc.ingin)
}
})
}
}
// Table-driven test untuk fungsi yang mengembalikan error
func TestBagiTableDriven(t *testing.T) {
tests := []struct {
nama string
a, b float64
ingin float64
inginErr bool
}{
{"pembagian normal", 10, 2, 5.0, false},
{"pembagian nol", 10, 0, 0, true},
{"negatif", -6, 2, -3.0, false},
{"pecahan", 1, 3, 0.3333333333333333, false},
}
for _, tc := range tests {
t.Run(tc.nama, func(t *testing.T) {
hasil, err := Bagi(tc.a, tc.b)
if tc.inginErr {
if err == nil {
t.Errorf("Bagi(%g, %g) ingin error, tapi tidak ada error", tc.a, tc.b)
}
return
}
if err != nil {
t.Fatalf("Bagi(%g, %g) error tidak diharapkan: %v", tc.a, tc.b, err)
}
if hasil != tc.ingin {
t.Errorf("Bagi(%g, %g) = %g, ingin %g", tc.a, tc.b, hasil, tc.ingin)
}
})
}
}
Sub-Test dengan t.Run #
t.Run membuat sub-test yang bisa dijalankan secara individual, memberikan output yang lebih terorganisir:
// Jalankan hanya sub-test tertentu:
// go test -run TestIsPalindrome/kata_tunggal
func TestIsPalindrome(t *testing.T) {
t.Run("kata tunggal", func(t *testing.T) {
if !IsPalindrome("a") {
t.Error("'a' seharusnya palindrome")
}
})
t.Run("kalimat palindrome", func(t *testing.T) {
if !IsPalindrome("kasur rusak") {
t.Error("'kasur rusak' seharusnya palindrome")
}
})
t.Run("bukan palindrome", func(t *testing.T) {
if IsPalindrome("halo") {
t.Error("'halo' bukan palindrome")
}
})
t.Run("string kosong", func(t *testing.T) {
if !IsPalindrome("") {
t.Error("string kosong seharusnya palindrome")
}
})
}
Test Helper — t.Helper #
t.Helper() menandai fungsi sebagai helper — sehingga saat test gagal, Go melaporkan baris di pemanggil helper, bukan di dalam helper:
// Tanpa t.Helper — output menunjuk ke assertEqual, bukan ke pemanggil
func assertEqual(t *testing.T, got, want interface{}) {
if got != want {
t.Errorf("got %v, want %v", got, want)
// Output: hitung_test.go:15: got 4, want 5
// Baris 15 adalah di dalam assertEqual, bukan di TestTambah!
}
}
// Dengan t.Helper — output menunjuk ke pemanggil
func assertEqualBaik(t *testing.T, got, want interface{}) {
t.Helper() // tandai sebagai helper
if got != want {
t.Errorf("got %v, want %v", got, want)
// Output: hitung_test.go:8: got 4, want 5
// Baris 8 adalah di TestTambah, lebih informatif!
}
}
// Helper yang lebih lengkap
func assertNoError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatalf("error tidak diharapkan: %v", err)
}
}
func assertError(t *testing.T, err error, contains string) {
t.Helper()
if err == nil {
t.Fatal("ingin error, tapi tidak ada error")
}
if contains != "" && !strings.Contains(err.Error(), contains) {
t.Errorf("error %q tidak mengandung %q", err.Error(), contains)
}
}
func assertEqual2(t *testing.T, got, want interface{}, format string, args ...interface{}) {
t.Helper()
if got != want {
msg := fmt.Sprintf(format, args...)
t.Errorf("%s: got %v, want %v", msg, got, want)
}
}
// Penggunaan
func TestDenganHelper(t *testing.T) {
hasil := Tambah(2, 3)
assertEqualBaik(t, hasil, 5) // baris ini yang dilaporkan jika gagal
_, err := Bagi(10, 0)
assertError(t, err, "nol")
}
t.Cleanup dan t.TempDir #
// t.Cleanup — jalankan fungsi saat test selesai (berhasil atau gagal)
// Lebih baik dari defer di banyak kasus karena terdaftar ke test runner
func TestDenganCleanup(t *testing.T) {
// Setup
server := mulaiTestServer()
t.Cleanup(func() {
server.Tutup() // otomatis dipanggil saat test selesai
})
db := buatTestDB()
t.Cleanup(func() {
db.DropTestTable()
db.Close()
})
// Test menggunakan server dan db
// Cleanup akan dipanggil dalam urutan LIFO (terakhir didaftarkan, pertama dipanggil)
}
// t.TempDir — buat direktori temporary yang otomatis dibersihkan
func TestOperasiFile(t *testing.T) {
tmpDir := t.TempDir()
// Direktori otomatis dihapus saat test selesai — tidak perlu cleanup manual
filePath := filepath.Join(tmpDir, "test.txt")
err := os.WriteFile(filePath, []byte("konten test"), 0644)
if err != nil {
t.Fatalf("gagal tulis file: %v", err)
}
// Baca kembali dan verifikasi
data, err := os.ReadFile(filePath)
if err != nil {
t.Fatalf("gagal baca file: %v", err)
}
if string(data) != "konten test" {
t.Errorf("konten file tidak sesuai: %q", data)
}
}
t.Parallel — Test Paralel #
t.Parallel() memungkinkan test dijalankan secara paralel dengan test lain yang juga memanggil Parallel():
func TestA(t *testing.T) {
t.Parallel() // test ini bisa dijalankan paralel dengan TestB dan TestC
time.Sleep(100 * time.Millisecond)
// ...
}
func TestB(t *testing.T) {
t.Parallel()
time.Sleep(100 * time.Millisecond)
// ...
}
// Tanpa t.Parallel: A → B → C = 300ms
// Dengan t.Parallel: A, B, C bersamaan = ~100ms
// PENTING: jangan capture loop variable di parallel sub-test!
func TestParalelTableDriven(t *testing.T) {
tests := []struct {
nama string
input int
}{
{"kasus 1", 1},
{"kasus 2", 2},
{"kasus 3", 3},
}
for _, tc := range tests {
tc := tc // WAJIB: shadow variable untuk parallel sub-test (Go < 1.22)
t.Run(tc.nama, func(t *testing.T) {
t.Parallel()
// Gunakan tc.input di sini — aman karena sudah di-shadow
_ = tc.input
})
}
}
t.Skip — Melewati Test #
func TestButuhDatabase(t *testing.T) {
// Skip jika environment variable tidak diset
if os.Getenv("DATABASE_URL") == "" {
t.Skip("skip: DATABASE_URL tidak diset")
}
db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
// ...
}
func TestButuhInternet(t *testing.T) {
if testing.Short() {
t.Skip("skip: mode -short")
}
// Test yang butuh koneksi internet
}
// Jalankan dengan: go test -short ./...
// untuk skip test yang lambat
Benchmark #
Benchmark mengukur performa kode — berapa lama per operasi, berapa banyak alokasi memori:
// BenchmarkXxx — nama harus dimulai dengan Benchmark
func BenchmarkTambah(b *testing.B) {
// b.N diatur otomatis oleh test runner
// mulai dari nilai kecil dan ditingkatkan sampai hasil stabil
for i := 0; i < b.N; i++ {
Tambah(2, 3)
}
}
// Benchmark dengan setup
func BenchmarkIsPalindrome(b *testing.B) {
input := "kasur rusak"
b.ResetTimer() // reset timer setelah setup (jika ada)
for i := 0; i < b.N; i++ {
IsPalindrome(input)
}
}
// Benchmark yang melaporkan alokasi memori
func BenchmarkBuatSlice(b *testing.B) {
b.ReportAllocs() // laporkan alokasi per operasi
for i := 0; i < b.N; i++ {
s := make([]int, 100)
_ = s
}
}
// Jalankan benchmark:
// go test -bench=. -benchmem ./...
//
// Output:
// BenchmarkTambah-8 1000000000 0.3 ns/op
// BenchmarkIsPalindrome-8 50000000 25.0 ns/op 0 allocs/op
// BenchmarkBuatSlice-8 10000000 120.0 ns/op 1 allocs/op 808 B/op
Benchmark Sub-test #
func BenchmarkStringsVsBytes(b *testing.B) {
data := strings.Repeat("a", 1000)
b.Run("strings.Contains", func(b *testing.B) {
for i := 0; i < b.N; i++ {
strings.Contains(data, "zzz")
}
})
b.Run("strings.Index", func(b *testing.B) {
for i := 0; i < b.N; i++ {
strings.Index(data, "zzz")
}
})
b.Run("regexp", func(b *testing.B) {
re := regexp.MustCompile("zzz")
b.ResetTimer()
for i := 0; i < b.N; i++ {
re.MatchString(data)
}
})
}
Benchmark Paralel #
func BenchmarkParalel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// Kode yang ditest secara paralel
Tambah(1, 2)
}
})
}
Fuzzing #
Fuzzing secara otomatis menghasilkan input yang tidak terduga untuk menemukan bug dan edge case. Go mendukung fuzzing built-in sejak Go 1.18:
// FuzzXxx — nama harus dimulai dengan Fuzz
func FuzzIsPalindrome(f *testing.F) {
// Seed corpus — contoh input awal
f.Add("kasur rusak")
f.Add("halo")
f.Add("")
f.Add("a")
f.Fuzz(func(t *testing.T, input string) {
// Fuzzer akan memanggil ini dengan berbagai variasi input
result := IsPalindrome(input)
// Properti yang harus selalu benar (invariant)
// Palindrome dari palindrome harus tetap palindrome
if result {
reversed := reverseString(input)
if !IsPalindrome(reversed) {
t.Errorf("palindrome %q terbalik %q bukan palindrome", input, reversed)
}
}
})
}
func reverseString(s string) string {
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes)
}
// Jalankan fuzzing:
// go test -fuzz=FuzzIsPalindrome -fuzztime=30s
Testing dengan Interface dan Mock #
Salah satu kekuatan testing di Go adalah interface — dengan mendefinisikan dependensi sebagai interface, kamu bisa mengganti implementasi asli dengan mock di test:
flowchart LR
subgraph Produksi["Produksi"]
Service["UserService"] --> RealDB["PostgresUserRepo\nimplementasi nyata"]
end
subgraph Test["Test"]
ServiceT["UserService"] --> MockDB["MockUserRepo\nimplementasi test"]
end
subgraph Interface["Interface"]
I["UserRepository\n+ FindByID(id) (*User, error)\n+ Save(user) error\n+ Delete(id) error"]
end
RealDB --> Interface
MockDB --> Interface
Service --> Interface
ServiceT --> Interface
style Interface fill:#4f86c6,color:#fff
style MockDB fill:#e8f5e9
style RealDB fill:#e3f2fd// Definisi interface
type UserRepository interface {
FindByID(ctx context.Context, id int) (*User, error)
Save(ctx context.Context, user *User) error
Delete(ctx context.Context, id int) error
}
// Implementasi produksi
type PostgresUserRepo struct {
db *sql.DB
}
// Implementasi mock untuk test
type MockUserRepo struct {
users map[int]*User
Errors map[string]error // injeksi error untuk test kasus gagal
}
func NewMockUserRepo() *MockUserRepo {
return &MockUserRepo{
users: make(map[int]*User),
Errors: make(map[string]error),
}
}
func (m *MockUserRepo) FindByID(ctx context.Context, id int) (*User, error) {
if err := m.Errors["FindByID"]; err != nil {
return nil, err
}
user, ada := m.users[id]
if !ada {
return nil, ErrTidakDitemukan
}
return user, nil
}
func (m *MockUserRepo) Save(ctx context.Context, user *User) error {
if err := m.Errors["Save"]; err != nil {
return err
}
m.users[user.ID] = user
return nil
}
func (m *MockUserRepo) Delete(ctx context.Context, id int) error {
if err := m.Errors["Delete"]; err != nil {
return err
}
delete(m.users, id)
return nil
}
// Service yang menggunakan interface
type UserService struct {
repo UserRepository
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) GetUser(ctx context.Context, id int) (*User, error) {
if id <= 0 {
return nil, fmt.Errorf("ID harus positif")
}
return s.repo.FindByID(ctx, id)
}
// Test menggunakan mock
func TestUserServiceGetUser(t *testing.T) {
ctx := context.Background()
t.Run("pengguna ditemukan", func(t *testing.T) {
mock := NewMockUserRepo()
mock.Save(ctx, &User{ID: 1, Nama: "Budi"})
svc := NewUserService(mock)
user, err := svc.GetUser(ctx, 1)
assertNoError(t, err)
if user.Nama != "Budi" {
t.Errorf("nama pengguna = %q, ingin 'Budi'", user.Nama)
}
})
t.Run("pengguna tidak ditemukan", func(t *testing.T) {
mock := NewMockUserRepo()
svc := NewUserService(mock)
_, err := svc.GetUser(ctx, 999)
assertError(t, err, "")
})
t.Run("ID tidak valid", func(t *testing.T) {
mock := NewMockUserRepo()
svc := NewUserService(mock)
_, err := svc.GetUser(ctx, -1)
assertError(t, err, "positif")
})
t.Run("error dari repository", func(t *testing.T) {
mock := NewMockUserRepo()
mock.Errors["FindByID"] = fmt.Errorf("koneksi database terputus")
svc := NewUserService(mock)
_, err := svc.GetUser(ctx, 1)
assertError(t, err, "")
})
}
Test Coverage #
# Jalankan test dengan coverage
go test -cover ./...
# Output:
# ok github.com/user/myapp/hitung 0.003s coverage: 85.7% of statements
# Buat laporan coverage HTML
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
# Coverage per fungsi
go tool cover -func=coverage.out
# Jalankan test hanya untuk package tertentu
go test ./internal/service/...
# Jalankan test dengan nama tertentu
go test -run TestTambah ./...
# Jalankan test dengan race detector
go test -race ./...
# Jalankan test dengan timeout
go test -timeout 30s ./...
# Kombinasi flag yang berguna
go test -v -race -cover -timeout 60s ./...
Pola Penggunaan di Produksi #
Test HTTP Handler #
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestHandlerDaftarProduk(t *testing.T) {
// httptest.NewRecorder — ResponseWriter palsu yang merekam respons
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/api/produk?limit=10", nil)
r.Header.Set("Authorization", "Bearer test-token")
// Jalankan handler
handlerDaftarProduk(w, r)
// Periksa status code
if w.Code != http.StatusOK {
t.Errorf("status code = %d, ingin %d", w.Code, http.StatusOK)
}
// Periksa Content-Type
contentType := w.Header().Get("Content-Type")
if !strings.Contains(contentType, "application/json") {
t.Errorf("Content-Type = %q, ingin JSON", contentType)
}
// Decode dan periksa respons
var respons []Produk
if err := json.NewDecoder(w.Body).Decode(&respons); err != nil {
t.Fatalf("gagal decode respons: %v", err)
}
if len(respons) == 0 {
t.Error("respons kosong")
}
}
// Test dengan router/mux lengkap
func TestServerIntegrasi(t *testing.T) {
mux := http.NewServeMux()
daftarkanRoute(mux)
// httptest.NewServer — server HTTP nyata di port acak
server := httptest.NewServer(mux)
defer server.Close()
// Kirim request nyata ke server test
resp, err := http.Get(server.URL + "/api/produk")
if err != nil {
t.Fatalf("request gagal: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("status = %d, ingin 200", resp.StatusCode)
}
}
Test dengan TestMain — Setup dan Teardown Global #
// TestMain dijalankan sebelum semua test dalam package
func TestMain(m *testing.M) {
// Setup global sebelum semua test
db = setupTestDatabase()
populasiDataTest()
// m.Run() menjalankan semua test
exitCode := m.Run()
// Cleanup setelah semua test selesai
bersihkanDatabase()
db.Close()
os.Exit(exitCode)
}
var db *sql.DB
func setupTestDatabase() *sql.DB {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
log.Fatalf("gagal buka test DB: %v", err)
}
// Jalankan migrasi
if err := jalankanMigrasi(db); err != nil {
log.Fatalf("gagal migrasi: %v", err)
}
return db
}
Golden File Test — Snapshot Testing #
// Golden file test: bandingkan output dengan file referensi
func TestFormatLaporan(t *testing.T) {
input := DataLaporan{
Judul: "Laporan Bulanan",
Periode: "Maret 2024",
Total: 15000000,
ItemCount: 42,
}
hasil := FormatLaporan(input)
goldenPath := filepath.Join("testdata", "laporan_bulanan.golden")
// Update golden file dengan: go test -update
if *flagUpdate {
os.MkdirAll("testdata", 0755)
os.WriteFile(goldenPath, []byte(hasil), 0644)
return
}
// Baca dan bandingkan dengan golden file
expected, err := os.ReadFile(goldenPath)
if err != nil {
t.Fatalf("gagal baca golden file: %v\n"+
"Jalankan 'go test -update' untuk membuat golden file", err)
}
if string(expected) != hasil {
t.Errorf("output tidak sesuai golden file\n\nGot:\n%s\n\nWant:\n%s",
hasil, expected)
}
}
var flagUpdate = flag.Bool("update", false, "update golden files")
Kapan Beralih ke Alternatif #
Tetap gunakan package testing jika:
✓ Unit test, benchmark, fuzzing — semua kasus umum
✓ Test yang ingin zero external dependency
✓ Idiomatis Go: table-driven test, t.Helper, t.Run
Pertimbangkan library assertion jika:
✗ Tim lebih produktif dengan assertion yang ekspresif
→ testify/assert — assert.Equal, assert.NoError, dll
→ testify/require — seperti assert tapi memanggil t.FailNow()
→ gomock — mock generator dari interface
Pertimbangkan testing framework lain jika:
✗ BDD-style test (Given/When/Then)
→ goconvey, ginkgo/gomega
✗ Property-based testing (seperti QuickCheck)
→ gopter, rapid
Pertimbangkan integration test runner jika:
✗ Test yang butuh Docker container (database, Redis, dll)
→ testcontainers-go
✗ End-to-end test browser
→ playwright-go, chromedp
Ringkasan #
- Table-driven test adalah pola paling idiomatis di Go — definisikan semua kasus dalam
[]struct{...}dan iterasi dengant.Rununtuk output yang terorganisir dan mudah diidentifikasi.t.Helper()di setiap fungsi helper agar pesan error menunjuk ke pemanggil helper, bukan ke dalam helper — ini membuat debugging jauh lebih mudah.t.Fataluntuk kegagalan yang menghentikan test,t.Erroruntuk yang bisa dilanjutkan — gunakanFatalsaat langkah berikutnya tidak masuk akal jika langkah ini gagal.t.Cleanuplebih baik darideferuntuk cleanup test karena selalu dijalankan bahkan jikat.Fataldipanggil, dan terdaftar ke test runner.t.TempDir()untuk direktori temporary yang otomatis dibersihkan — tidak perlu cleanup manual, lebih bersih darios.TempDir()manual.- Interface untuk testability — dependensi yang didefinisikan sebagai interface memungkinkan penggantian dengan mock di test tanpa mengubah kode produksi.
httptest.NewRecorderdanhttptest.NewRequestuntuk test HTTP handler — tidak perlu server nyata, test lebih cepat dan terisolasi.go test -raceuntuk mendeteksi race condition — jalankan ini di CI/CD, terutama untuk kode concurrent.- Benchmark dengan
b.ReportAllocs()untuk memantau alokasi memori — penting untuk kode yang dipanggil ribuan kali per detik.- Fuzzing untuk menemukan edge case yang tidak terpikirkan — sangat efektif untuk fungsi parsing, encoding/decoding, dan manipulasi data.