Uji Unit dengan Go: Struktur & Praktik Terbaik

Pengujian Go dari dasar hingga pola lanjutan

Konten Halaman

Package pengujian bawaan Go menyediakan kerangka kerja yang kuat dan minimalis untuk menulis pengujian unit tanpa ketergantungan eksternal. Berikut adalah dasar-dasar pengujian, struktur proyek, dan pola lanjutan untuk membangun aplikasi Go yang andal.

Pengujian Unit Go sangat hebat

Mengapa Pengujian Penting dalam Go

Filosofi Go menekankan kesederhanaan dan keandalan. Perpustakaan standar mencakup paket testing, membuat pengujian unit menjadi warga negara kelas satu dalam ekosistem Go. Kode Go yang telah diuji dengan baik meningkatkan keterpeliharaan, menangkap bug sejak dini, dan menyediakan dokumentasi melalui contoh. Jika Anda baru dengan Go, lihat Kartu Panduan Go kami untuk referensi cepat tentang dasar-dasar bahasa tersebut.

Manfaat utama pengujian Go:

  • Dukungan bawa-in: Tidak diperlukan kerangka kerja eksternal
  • Eksekusi cepat: Eksekusi pengujian secara paralel secara default
  • Sintaks sederhana: Kode boilerplate minimal
  • Alat yang kaya: Laporan cakupan, benchmark, dan profiling
  • Ramah CI/CD: Integrasi mudah dengan pipeline otomatis

Struktur Proyek untuk Pengujian Go

Pengujian Go berada di samping kode produksi Anda dengan konvensi penamaan yang jelas:

myproject/
├── go.mod
├── main.go
├── calculator.go
├── calculator_test.go
├── utils/
│   ├── helper.go
│   └── helper_test.go
└── models/
    ├── user.go
    └── user_test.go

Konvensi penting:

  • File pengujian diakhiri dengan _test.go
  • Pengujian berada dalam paket yang sama dengan kode (atau menggunakan sufiks _test untuk pengujian kotak hitam)
  • Setiap file sumber dapat memiliki file pengujian yang sesuai

Pendekatan Pengujian Paket

Pengujian kotak putih (sama paket):

package calculator

import "testing"
// Bisa mengakses fungsi dan variabel yang tidak diekspor

Pengujian kotak hitam (paket eksternal):

package calculator_test

import (
    "testing"
    "myproject/calculator"
)
// Hanya bisa mengakses fungsi yang diekspor (direkomendasikan untuk API publik)

Struktur Dasar Pengujian

Setiap fungsi pengujian mengikuti pola ini:

package calculator

import "testing"

// Fungsi pengujian harus dimulai dengan "Test"
func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5
    
    if result != expected {
        t.Errorf("Add(2, 3) = %d; want %d", result, expected)
    }
}

Metode testing.T:

  • t.Error() / t.Errorf(): Tandai pengujian sebagai gagal tetapi lanjutkan
  • t.Fatal() / t.Fatalf(): Tandai pengujian sebagai gagal dan berhenti segera
  • t.Log() / t.Logf(): Log output (hanya ditampilkan dengan flag -v)
  • t.Skip() / t.Skipf(): Lewati pengujian
  • t.Parallel(): Jalankan pengujian secara paralel dengan pengujian paralel lainnya

Pengujian Berbasis Tabel: Cara Go

Pengujian berbasis tabel adalah pendekatan idiomatic Go untuk menguji berbagai skenario. Dengan Generik Go, Anda juga dapat membuat bantuan pengujian yang aman secara tipe yang bekerja di berbagai jenis data:

func TestCalculate(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        op       string
        expected int
        wantErr  bool
    }{
        {"penjumlahan", 2, 3, "+", 5, false},
        {"pengurangan", 5, 3, "-", 2, false},
        {"perkalian", 4, 3, "*", 12, false},
        {"pembagian", 10, 2, "/", 5, false},
        {"pembagian dengan nol", 10, 0, "/", 0, true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result, err := Calculate(tt.a, tt.b, tt.op)
            
            if (err != nil) != tt.wantErr {
                t.Errorf("Calculate() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            
            if result != tt.expected {
                t.Errorf("Calculate(%d, %d, %q) = %d; want %d", 
                    tt.a, tt.b, tt.op, result, tt.expected)
            }
        })
    }
}

Keuntungan:

  • Satu fungsi pengujian untuk berbagai skenario
  • Mudah menambahkan kasus pengujian baru
  • Dokumentasi jelas tentang perilaku yang diharapkan
  • Organisasi dan pemeliharaan pengujian yang lebih baik

Menjalankan Pengujian

Perintah Dasar

# Jalankan pengujian di direktori saat ini
go test

# Jalankan pengujian dengan output rinci
go test -v

# Jalankan pengujian di semua subdirektori
go test ./...

# Jalankan pengujian spesifik
go test -run TestAdd

# Jalankan pengujian dengan pola
go test -run TestCalculate/addition

# Jalankan pengujian secara paralel (default adalah GOMAXPROCS)
go test -parallel 4

# Jalankan pengujian dengan timeout
go test -timeout 30s

Cakupan Pengujian

# Jalankan pengujian dengan cakupan
go test -cover

# Buat profil cakupan
go test -coverprofile=coverage.out

# Tampilkan cakupan di browser
go tool cover -html=coverage.out

# Tampilkan cakupan berdasarkan fungsi
go tool cover -func=coverage.out

# Tetapkan mode cakupan (set, count, atomic)
go test -covermode=count -coverprofile=coverage.out

Flag yang Berguna

  • -short: Jalankan pengujian yang ditandai dengan if testing.Short()
  • -race: Aktifkan detektor race (mencari masalah akses konkuren)
  • -cpu: Tentukan nilai GOMAXPROCS
  • -count n: Jalankan setiap pengujian n kali
  • -failfast: Berhenti pada kegagalan pengujian pertama

Bantuan Pengujian dan Setup/Teardown

Fungsi Bantuan

Tandai fungsi bantuan dengan t.Helper() untuk meningkatkan pelaporan kesalahan:

func assertEqual(t *testing.T, got, want int) {
    t.Helper() // Baris ini dilaporkan sebagai pemanggil
    if got != want {
        t.Errorf("got %d, want %d", got, want)
    }
}

func TestMath(t *testing.T) {
    result := Add(2, 3)
    assertEqual(t, result, 5) // Baris kesalahan menunjuk ke sini
}

Setup dan Teardown

func TestMain(m *testing.M) {
    // Kode setup di sini
    setup()
    
    // Jalankan pengujian
    code := m.Run()
    
    // Kode teardown di sini
    teardown()
    
    os.Exit(code)
}

Fixtures Pengujian

func setupTestCase(t *testing.T) func(t *testing.T) {
    t.Log("setup test case")
    return func(t *testing.T) {
        t.Log("teardown test case")
    }
}

func TestSomething(t *testing.T) {
    teardown := setupTestCase(t)
    defer teardown(t)
    
    // Kode pengujian di sini
}

Pemalsuan dan Injeksi Ketergantungan

Pemalsuan Berbasis Interface

Ketika menguji kode yang berinteraksi dengan database, menggunakan interface membuatnya mudah untuk membuat implementasi pemalsuan. Jika Anda bekerja dengan PostgreSQL dalam Go, lihat perbandingan ORMs Go kami untuk memilih perpustakaan database yang tepat dengan tingkat pengujian yang baik.

// Kode produksi
type Database interface {
    GetUser(id int) (*User, error)
}

type UserService struct {
    db Database
}

func (s *UserService) GetUserName(id int) (string, error) {
    user, err := s.db.GetUser(id)
    if err != nil {
        return "", err
    }
    return user.Name, nil
}

// Kode pengujian
type MockDatabase struct {
    users map[int]*User
}

func (m *MockDatabase) GetUser(id int) (*User, error) {
    if user, ok := m.users[id]; ok {
        return user, nil
    }
    return nil, errors.New("user not found")
}

func TestGetUserName(t *testing.T) {
    mockDB := &MockDatabase{
        users: map[int]*User{
            1: {ID: 1, Name: "Alice"},
        },
    }
    
    service := &UserService{db: mockDB}
    name, err := service.GetUserName(1)
    
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if name != "Alice" {
        t.Errorf("got %s, want Alice", name)
    }
}

Perpustakaan Pengujian Populer

Testify

Perpustakaan pengujian Go paling populer untuk asersi dan pemalsuan:

import (
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
)

func TestWithTestify(t *testing.T) {
    result := Add(2, 3)
    assert.Equal(t, 5, result, "they should be equal")
    assert.NotNil(t, result)
}

// Contoh pemalsuan
type MockDB struct {
    mock.Mock
}

func (m *MockDB) GetUser(id int) (*User, error) {
    args := m.Called(id)
    return args.Get(0).(*User), args.Error(1)
}

Alat Lain

  • gomock: Kerangka kerja pemalsuan Google dengan pembangkitan kode
  • httptest: Perpustakaan standar untuk menguji penangan HTTP
  • testcontainers-go: Pengujian integrasi dengan kontainer Docker
  • ginkgo/gomega: Kerangka kerja pengujian gaya BDD

Ketika menguji integrasi dengan layanan eksternal seperti model AI, Anda perlu memalsukan atau menstub ketergantungan tersebut. Misalnya, jika Anda menggunakan Ollama dalam Go, pertimbangkan membuat pengepakan interface untuk membuat kode Anda lebih mudah diuji.

Pengujian Benchmark

Go menyediakan dukungan bawa-in untuk benchmark:

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(2, 3)
    }
}

// Jalankan benchmark
// go test -bench=. -benchmem

Output menunjukkan iterasi per detik dan alokasi memori.

Praktik Terbaik

  1. Tulis pengujian berbasis tabel: Gunakan pola slice of structs untuk berbagai kasus pengujian
  2. Gunakan t.Run untuk subtest: Organisasi yang lebih baik dan bisa menjalankan subtest secara selektif
  3. Uji fungsi yang diekspor terlebih dahulu: Fokus pada perilaku API publik
  4. Jaga pengujian sederhana: Setiap pengujian harus memverifikasi satu hal
  5. Gunakan nama pengujian yang bermakna: Deskripsikan apa yang diuji dan hasil yang diharapkan
  6. Jangan uji detail implementasi: Uji perilaku, bukan internal
  7. Gunakan interface untuk ketergantungan: Membuat pemalsuan lebih mudah
  8. Arahkan pada cakupan tinggi, tetapi kualitas lebih dari kuantitas: Cakupan 100% tidak berarti bebas bug
  9. Jalankan pengujian dengan flag -race: Tangkap masalah konkuren sejak dini
  10. Gunakan TestMain untuk setup mahal: Hindari mengulang setup di setiap pengujian

Contoh: Suite Pengujian Lengkap

package user

import (
    "errors"
    "testing"
)

type User struct {
    ID    int
    Name  string
    Email string
}

func ValidateUser(u *User) error {
    if u.Name == "" {
        return errors.New("name cannot be empty")
    }
    if u.Email == "" {
        return errors.New("email cannot be empty")
    }
    return nil
}

// File pengujian: user_test.go
func TestValidateUser(t *testing.T) {
    tests := []struct {
        name    string
        user    *User
        wantErr bool
        errMsg  string
    }{
        {
            name:    "user valid",
            user:    &User{ID: 1, Name: "Alice", Email: "alice@example.com"},
            wantErr: false,
        },
        {
            name:    "nama kosong",
            user:    &User{ID: 1, Name: "", Email: "alice@example.com"},
            wantErr: true,
            errMsg:  "name cannot be empty",
        },
        {
            name:    "email kosong",
            user:    &User{ID: 1, Name: "Alice", Email: ""},
            wantErr: true,
            errMsg:  "email cannot be empty",
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := ValidateUser(tt.user)
            
            if (err != nil) != tt.wantErr {
                t.Errorf("ValidateUser() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            
            if err != nil && err.Error() != tt.errMsg {
                t.Errorf("ValidateUser() error message = %v, want %v", err.Error(), tt.errMsg)
            }
        })
    }
}

Tautan Berguna

Kesimpulan

Kerangka kerja pengujian Go menyediakan segala yang diperlukan untuk pengujian unit menyeluruh dengan pengaturan minimal. Dengan mengikuti idiom Go seperti pengujian berbasis tabel, menggunakan interface untuk pemalsuan, dan memanfaatkan alat bawa-in, Anda dapat menciptakan suite pengujian yang dapat dipelihara dan andal yang tumbuh bersama dengan kode Anda.

Praktik pengujian ini berlaku untuk semua jenis aplikasi Go, dari layanan web hingga aplikasi CLI yang dibangun dengan Cobra & Viper. Pengujian alat baris perintah memerlukan pola serupa dengan fokus tambahan pada pengujian input/output dan parsing flag.

Mulailah dengan pengujian sederhana, secara bertahap tambahkan cakupan, dan ingat bahwa pengujian adalah investasi dalam kualitas kode dan kepercayaan pengembang. Penekanan komunitas Go pada pengujian membuatnya lebih mudah untuk mempertahankan proyek jangka panjang dan bekerja sama secara efektif dengan rekan tim.