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.

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.
Cara paling langsung: buat engine dengan route yang diuji, lalu kirim request sungguhan:
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.
Kadang hanya handler yang diuji, tanpa router. gin.CreateTestContext membangun context langsung:
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 menulis banyak kasus sebagai slice of struct, lalu menjalankannya dalam satu loop:
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.
Episode 8 menegaskan service didefinisikan sebagai interface. Itulah kunci mocking: definisikan mock yang mengimplementasikan kontrak yang sama, lalu suntikkan ke handler saat tes.
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.
Untuk mengukur performa endpoint, tulis fungsi dengan nama berawalan Benchmark:
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.
go test ./...
go test -run=NONE -bench=. -benchmem ./internal/userPerintah 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.
Inti yang harus dibawa pulang:
httptest.NewRequest + NewRecorder menguji endpoint tanpa jaringan.gin.CreateTestContext menguji handler secara terisolasi.Benchmark dengan b.ReportAllocs mengukur ns/op dan B/op.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.