← Back to Cecep Azhar
Cecep Azhar #golang#testing#unit-test#tdd#best-practices

Testing di Go: Cara Menulis Unit Test yang Efektif

Cecep Saeful Azhar • 16 Oct 2026
Testing di Go: Cara Menulis Unit Test yang Efektif
Panduan testing di Go. Unit test, table-driven tests, mocking, dan coverage report untuk code production-ready.

Testing: Investasi yang Selalu Membayar

Saya pernah skip testing karena deadline ketat. Hasilnya? Bug production yang memakan waktu 3x lebih lama untuk fix daripada menulis test. Sejak itu, test-first menjadi workflow saya.

Dasar Table-Driven Tests

Go community sangat favor table-driven tests. Ini pattern yang paling saya pakai:

func TestCalculateDiscount(t *testing.T) {
    tests := []struct {
        name     string
        plan     string
        annual   bool
        expected float64
    }{
        {name: "basic plan", plan: "basic", annual: false, expected: 99000},
        {name: "pro plan annual", plan: "pro", annual: true, expected: 239200},
        {name: "enterprise", plan: "enterprise", annual: false, expected: 0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := CalculateDiscount(tt.plan, tt.annual)
            if result != tt.expected {
                t.Errorf("got %v, want %v", result, tt.expected)
            }
        })
    }
}

Mocking: Isolate Your Tests

// Interface untuk dependency
type UserRepository interface {
    FindByID(id string) (*User, error)
    Create(user *User) error
}

// Mock implementation
type MockUserRepo struct {
    Users map[string]*User
}

func (m *MockUserRepo) FindByID(id string) (*User, error) {
    user, ok := m.Users[id]
    if !ok { return nil, ErrUserNotFound }
    return user, nil
}

func TestUserService_GetUser(t *testing.T) {
    repo := &MockUserRepo{
        Users: map[string]*User{"1": {Name: "Cecep"}},
    }
    svc := NewUserService(repo)

    t.Run("found", func(t *testing.T) {
        user, err := svc.GetUser("1")
        if err != nil { t.Fatal(err) }
        if user.Name != "Cecep" {
            t.Errorf("expected Cecep, got %s", user.Name)
        }
    })

    t.Run("not found", func(t *testing.T) {
        _, err := svc.GetUser("999")
        if !errors.Is(err, ErrUserNotFound) {
            t.Errorf("expected ErrUserNotFound, got %v", err)
        }
    })
}

Integration Test dengan Real Database

func TestIntegration_CreateUser(t *testing.T) {
    if testing.Short() {
        t.Skip("Skipping integration test")
    }
    db := setupTestDB(t)
    defer cleanupTestDB(t, db)

    repo := NewPostgresUserRepo(db)
    user := &User{Name: "Test User", Email: "[email protected]"}
    err := repo.Create(user)
    if err != nil { t.Fatalf("failed to create user: %v", err) }
    if user.ID == 0 { t.Error("expected user ID to be set") }
}

Tips Testing yang Saya Ikuti

Benchmark Testing untuk Performance

Go juga punya built-in benchmark testing. Ini sangat berguna untuk mengukur performa code critical path:

func BenchmarkCalculateDiscount(b *testing.B) {
    for i := 0; i < b.N; i++ {
        CalculateDiscount("pro", true)
    }
}

// Jalankan: go test -bench=BenchmarkCalculateDiscount -benchmem
// Output: BenchmarkCalculateDiscount-8  50000000  28.5 ns/op  0 B/op  0 allocs/op

Benchmark test membantu Anda detect performance regression sebelum masuk ke production. Jalankan benchmark di setiap PR untuk compare performa sebelum dan sesudah perubahan.

Test Coverage yang Meaningful

Jangan hanya mengejar angka coverage tinggi. Yang lebih penting adalah apa yang di-cover. Prioritas testing:

  1. Business logic harus 90%+ coverage — ini inti dari produk Anda
  2. API handlers harus test semua happy path dan error cases
  3. Database queries test dengan integration test pakai real database
  4. Utility functions coverage tinggi karena mudah di-test

Saya tidak peduli dengan coverage di main.go atau wiring code. Yang penting adalah business logic dan API contract sudah ter-test dengan baik.

← More articles from Cecep Azhar Central Content Hub Home →