← Back to Cecep Azhar
Cecep Azhar #golang#testing#unit-test#mocking#integration-test#tdd

Testing di Golang: Strategi Komprehensif dari Unit sampai Integration

Cecep Saeful Azhar • 16 Oct 2026
Testing di Golang: Strategi Komprehensif dari Unit sampai Integration
Panduan testing Go yang lengkap: unit test, table-driven test, mock, integration test, dan test coverage. Tips production-ready.

Testing: Investasi yang Terbayar di Production

Saya sudah terlalu sering melihat kode tanpa test yang "works on my machine" tapi crash di production. Testing bukan tentang menulis kode tambahan — ini tentang menulis kode yang bisa diandalkan. Di Go, testing sudah built-in dan powerful. Mari kita manfaatkan sepenuhnya.

1. Table-Driven Tests (Go's Signature)

Table-driven tests adalah pattern testing yang paling "Go" banget. Clean, maintainable, dan scalable:

func TestCalculateDiscount(t *testing.T) {
    tests := []struct {
        name     string
        total    float64
        customer string
        want     float64
        wantErr  bool
    }{
        {
            name:     "regular customer under threshold",
            total:    100,
            customer: "regular",
            want:     100,
            wantErr:  false,
        },
        {
            name:     "vip customer gets 20pct discount",
            total:    500,
            customer: "vip",
            want:     400,
            wantErr:  false,
        },
        {
            name:     "negative total returns error",
            total:    -100,
            customer: "regular",
            want:     0,
            wantErr:  true,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := CalculateDiscount(tt.total, tt.customer)
            if (err != nil) != tt.wantErr {
                t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if got != tt.want {
                t.Errorf("CalculateDiscount() = %v, want %v", got, tt.want)
            }
        })
    }
}

2. Subtests untuk Test Organization

Subtests memudahkan menjalankan test spesifik dan memberikan output yang jelas:

func TestUserService(t *testing.T) {
    t.Run("CreateUser", func(t *testing.T) {
        // Test user creation
    })
    t.Run("CreateUser_DuplicateEmail", func(t *testing.T) {
        // Test duplicate email handling
    })
    t.Run("GetUser", func(t *testing.T) {
        t.Run("ExistingUser", func(t *testing.T) {
            // Test getting existing user
        })
        t.Run("NonExistingUser", func(t *testing.T) {
            // Test getting non-existing user
        })
    })
}

// go test -run TestUserService/CreateUser_DuplicateEmail

3. Mocking dengan Interfaces

Di Go, interfaces adalah best friend untuk testing. Define interface untuk dependency, mock-nya di test:

type UserRepository interface {
    FindByID(ctx context.Context, id string) (*User, error)
    Save(ctx context.Context, user *User) error
}

type MockUserRepo struct {
    users map[string]*User
    err   error
}

func (m *MockUserRepo) FindByID(ctx context.Context, id string) (*User, error) {
    if m.err != nil {
        return nil, m.err
    }
    user, ok := m.users[id]
    if !ok {
        return nil, ErrNotFound
    }
    return user, nil
}

func TestGetUser(t *testing.T) {
    mockRepo := &MockUserRepo{
        users: map[string]*User{
            "123": {ID: "123", Name: "Cecep"},
        },
    }
    svc := NewUserService(mockRepo)
    user, err := svc.GetUser(context.Background(), "123")
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if user.Name != "Cecep" {
        t.Errorf("got name %s, want Cecep", user.Name)
    }
}

4. Integration Test dengan Testcontainers

Untuk test yang melibatkan database, saya pakai Testcontainers — test dengan real database tapi diisolasi per test run:

func TestDatabaseIntegration(t *testing.T) {
    ctx := context.Background()
    pgContainer, _ := postgres.Run(ctx,
        "postgres:15-alpine",
        postgres.WithDatabase("testdb"),
        postgres.WithUsername("test"),
        postgres.WithPassword("test"),
    )
    defer pgContainer.Terminate(ctx)

    db, _ := sql.Open("pgx", connStr)
    repo := NewPostgresUserRepo(db)
    user := &User{Name: "Test User"}
    err := repo.Save(ctx, user)
    if err != nil {
        t.Fatalf("Save failed: %v", err)
    }
}

Test Coverage Strategy

Dari pengalaman saya, target coverage 80% itu ideal untuk production. Tapi jangan kejar angka — fokus pada meaningful tests yang mengcover edge cases dan business logic. Unit tests untuk logic, integration tests untuk data layer, dan end-to-end tests untuk critical paths saja.

← More articles from Cecep Azhar Central Content Hub Home →