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
- Jalankan go test -race -cover ./... di setiap commit
- Aim untuk 70%+ coverage di business logic, tapi jangan paksa 100% di semua package
- Pakai t.Parallel() untuk test yang independent ini mempercepat test suite signifikan
- Test harus deterministic tidak ada random data atau dependency ke external service
- Gunakan go test -short untuk quick test, full suite untuk CI/CD
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:
- Business logic harus 90%+ coverage — ini inti dari produk Anda
- API handlers harus test semua happy path dan error cases
- Database queries test dengan integration test pakai real database
- 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.