Belajar Gin - Testing & Benchmark
Series/Belajar Gin/Episode 17
Episode 17 of 23

Belajar Gin - Testing & Benchmark

Episode ini menguji aplikasi Gin dengan benar: unit test handler memakai httptest, gin.CreateTestContext untuk menguji handler langsung, table-driven test, mocking service dengan interface, serta benchmark route memakai testing.B.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Pendahuluan

Kode yang tidak diuji adalah bom waktu. Episode 17 ini membedah testing & benchmark aplikasi Gin: menguji endpoint dengan httptest, menguji handler secara terisolasi dengan gin.CreateTestContext, menulis table-driven test yang elegan, melakukan mocking service memakai interface dari episode 8, dan mengukur performa dengan testing.B.

Berbeda dari test manual dengan curl, test otomatis bisa dijalankan di pipeline CI setiap kali ada perubahan kode. Struktur lapisan yang kalian bangun di episode 8 akan terbayar di sini: karena handler menerima service lewat interface, mengganti service asli dengan mock sangat mudah.

Unit Test Handler dengan httptest

Membangun Engine Kecil

Cara paling langsung: buat engine dengan route yang diuji, lalu kirim request sungguhan:

Test endpoint penuh
func setupRouter() *gin.Engine {
    r := gin.New()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "pong"})
    })
    return r
}
 
func TestPing(t *testing.T) {
    r := setupRouter()
 
    req := httptest.NewRequest("GET", "/ping", nil)
    w := httptest.NewRecorder()
    r.ServeHTTP(w, req)
 
    if w.Code != http.StatusOK {
        t.Errorf("kode status: got %d, want %d", w.Code, http.StatusOK)
    }
}

httptest.NewRequest("GET", "/ping", nil) membuat request sungguhan tanpa jaringan, dan httptest.NewRecorder() menangkap response. r.ServeHTTP(w, req) menjalankan seluruh middleware dan handler. Pola ini menguji stack penuh — termasuk middleware CORS atau autentikasi.

gin.CreateTestContext

Menguji Handler Secara Isolasi

Kadang hanya handler yang diuji, tanpa router. gin.CreateTestContext membangun context langsung:

CreateTestContext
func TestGetUserHandler(t *testing.T) {
    w := httptest.NewRecorder()
    c, _ := gin.CreateTestContext(w)
 
    c.Request = httptest.NewRequest("GET", "/users/7", nil)
    c.Params = gin.Params{{Key: "id", Value: "7"}}
 
    handler := NewHandler(stubService())
    handler.GetUser(c)
 
    if w.Code != http.StatusOK {
        t.Fatalf("kode status: got %d, want %d", w.Code, http.StatusOK)
    }
}

gin.CreateTestContext(w) mengembalikan context yang siap dipakai handler. Path parameter diisi lewat c.Params, dan c.Request diisi request buatan. Pendekatan ini paling cepat untuk menguji handler tunggal tanpa middleware lain.

Table-Driven Test

Kasus dalam Satu Loop

Table-driven test menulis banyak kasus sebagai slice of struct, lalu menjalankannya dalam satu loop:

Table-driven test
func TestGetUserHandler(t *testing.T) {
    tests := []struct {
        name string
        id   string
        want int
    }{
        {"id valid", "1", 200},
        {"id bukan angka", "abc", 400},
        {"user tidak ada", "999", 404},
    }
 
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            w := httptest.NewRecorder()
            c, _ := gin.CreateTestContext(w)
            c.Request = httptest.NewRequest("GET", "/users/"+tt.id, nil)
            c.Params = gin.Params{{Key: "id", Value: tt.id}}
 
            NewHandler(fakeRepo()).GetUser(c)
 
            if w.Code != tt.want {
                t.Errorf("kasus %s: got %d, want %d", tt.name, w.Code, tt.want)
            }
        })
    }
}

t.Run(tt.name, ...) menjalankan setiap kasus sebagai subtest yang namanya muncul di output saat gagal. Menambah kasus baru cukup menambah satu baris di slice tests — tidak perlu menyalin blok test.

Mocking Service

Episode 8 menegaskan service didefinisikan sebagai interface. Itulah kunci mocking: definisikan mock yang mengimplementasikan kontrak yang sama, lalu suntikkan ke handler saat tes.

Mock service
type mockUserService struct {
    user *User
    err  error
}
 
func (m *mockUserService) GetUser(ctx context.Context, id int64) (*User, error) {
    return m.user, m.err
}
 
func stubService() UserService {
    return &mockUserService{
        user: &User{ID: 7, Name: "Arman"},
    }
}

mockUserService.GetUser mengembalikan data yang sudah ditentukan tanpa menyentuh database. Setiap test mengatur user dan err untuk mensimulasikan sukses atau kegagalan. Karena handler hanya mengenal interface, ia tidak bisa membedakan mock dari implementasi asli.

Benchmark dengan testing.B

Benchmark Route

Untuk mengukur performa endpoint, tulis fungsi dengan nama berawalan Benchmark:

Benchmark route
func BenchmarkPing(b *testing.B) {
    r := setupRouter()
 
    req := httptest.NewRequest("GET", "/ping", nil)
    w := httptest.NewRecorder()
 
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        r.ServeHTTP(w, req)
    }
}

b.ReportAllocs() mencatat alokasi memori per operasi. Loop b.N diatur otomatis oleh Go hingga benchmark selesai dengan waktu yang stabil. Perhatikan recorder w dipakai berulang — ini cukup untuk mengukur routing dan rendering.

Menjalankan dan Membaca Hasil

Jalankan test dan benchmark
go test ./...
go test -run=NONE -bench=. -benchmem ./internal/user

Perintah go test -run=NONE -bench=. -benchmem ./internal/user hanya menjalankan benchmark (bukan test) dengan pelaporan alokasi. Output seperti 27364 ns/op dan 0 B/op memberi gambaran latensi dan efisiensi memori per request. Episode 19 akan memakai angka ini untuk optimasi.

Penutup

Inti yang harus dibawa pulang:

  • httptest.NewRequest + NewRecorder menguji endpoint tanpa jaringan.
  • gin.CreateTestContext menguji handler secara terisolasi.
  • Table-driven test menjadikan kasus baru sekadar baris tambahan.
  • Interface dari clean architecture memungkinkan mocking service dengan mudah.
  • Benchmark dengan b.ReportAllocs mengukur ns/op dan B/op.
  • Jalankan go test ./... di CI agar regresi tertangkap sejak dini.

Di episode 18 selanjutnya kita akan membedah observability & monitoring — metrik Prometheus dengan prometheus/client_golang, tracing OpenTelemetry dengan otelgin, structured logs, serta endpoint health check untuk integrasi Kubernetes.