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

Belajar Chi - Testing & Benchmark

Episode ini memastikan aplikasi tidak rusak saat berkembang: unit test handler dengan httptest, menguji middleware dan subrouter, serta mocking service. Kalian juga belajar benchmark dengan testing.B untuk mengukur alokasi dan latensi rute.

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

Pendahuluan

Router yang bagus tidak ada artinya jika tidak bisa diuji dengan cepat dan andal. Episode 17 menunjukkan keunggulan utama desain chi: karena router hanyalah http.Handler, ia bisa diuji langsung dengan httptest tanpa membuka port, tanpa menunggu database, dan tanpa setup yang rumit. Kalian akan menulis test untuk handler, middleware, dan subrouter, lalu menutup dengan benchmark untuk mengukur performa rute — membandingkan alokasi dan latensi secara objektif.

Unit Test Handler dengan httptest

Pendekatan Tanpa Port

Test handler dengan httptest
func TestHandlerPing(t *testing.T) {
    r := chi.NewRouter()
    r.Get("/ping", func(w http.ResponseWriter, req *http.Request) {
        w.Write([]byte("pong"))
    })
 
    req := httptest.NewRequest(http.MethodGet, "/ping", nil)
    rec := httptest.NewRecorder()
 
    r.ServeHTTP(rec, req)
 
    if rec.Code != http.StatusOK {
        t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
    }
    if rec.Body.String() != "pong" {
        t.Fatalf("body = %q, want pong", rec.Body.String())
    }
}

httptest.NewRequest(http.MethodGet, "/ping", nil) membuat request tiruan, lalu r.ServeHTTP(rec, req) menjalankan seluruh middleware stack tanpa server sungguhan. Hasilnya dibaca dari rec.Code dan rec.Body.

Test Parameter Route

Test route berparam
func TestGetUser(t *testing.T) {
    r := chi.NewRouter()
    r.Get("/users/{id}", func(w http.ResponseWriter, req *http.Request) {
        w.Write([]byte(chi.URLParam(req, "id")))
    })
 
    req := httptest.NewRequest(http.MethodGet, "/users/42", nil)
    rec := httptest.NewRecorder()
    r.ServeHTTP(rec, req)
 
    if rec.Body.String() != "42" {
        t.Fatalf("body = %q, want 42", rec.Body.String())
    }
}

chi.URLParam(req, "id") bekerja normal karena route context dibangun oleh router selama ServeHTTP.

Menguji Middleware dan Subrouter

Middleware Berdiri Sendiri

Test middleware
func TestSecurityHeaders(t *testing.T) {
    inner := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
        w.WriteHeader(http.StatusOK)
    })
 
    req := httptest.NewRequest(http.MethodGet, "/", nil)
    rec := httptest.NewRecorder()
 
    securityHeaders(inner).ServeHTTP(rec, req)
 
    if rec.Header().Get("X-Frame-Options") != "DENY" {
        t.Fatalf("X-Frame-Options = %q, want DENY",
            rec.Header().Get("X-Frame-Options"))
    }
}

securityHeaders(inner).ServeHTTP(rec, req) membungkus stub dengan middleware lalu memanggilnya langsung — tidak perlu router utuh untuk membuktikan middleware bekerja.

Menguji Subrouter

Test subrouter
func TestUsersRouter(t *testing.T) {
    r := chi.NewRouter()
    r.Mount("/users", usersRouter())
 
    req := httptest.NewRequest(http.MethodGet, "/users/", nil)
    rec := httptest.NewRecorder()
    r.ServeHTTP(rec, req)
 
    if rec.Code != http.StatusOK {
        t.Fatalf("status = %d, want 200", rec.Code)
    }
}

r.Mount("/users", usersRouter()) memakai router resource yang sama dengan produksi (episode 5), sehingga test mencerminkan perilaku asli termasuk middleware subrouter.

Mocking Service

Interface untuk Mock

Mock service
type mockUserService struct{}
 
func (m mockUserService) GetByID(ctx context.Context, id int) (User, error) {
    return User{ID: id, Name: "Budi"}, nil
}
 
func TestUserHandlerGet(t *testing.T) {
    h := UserHandler{svc: mockUserService{}}
    req := httptest.NewRequest(http.MethodGet, "/users/7", nil)
    rec := httptest.NewRecorder()
 
    h.GetByID(rec, req)
 
    if rec.Code != http.StatusOK {
        t.Fatalf("status = %d, want 200", rec.Code)
    }
}

mockUserService{} menggantikan implementasi database dengan data tetap. Test handler kini cepat, deterministik, dan tanpa dependency eksternal.

Benchmark dengan testing.B

Mengukur Rute

Benchmark rute
func BenchmarkRouterGet(b *testing.B) {
    r := chi.NewRouter()
    r.Get("/users/{id}", func(w http.ResponseWriter, req *http.Request) {
        w.WriteHeader(http.StatusNoContent)
    })
 
    req := httptest.NewRequest(http.MethodGet, "/users/42", nil)
 
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        rec := httptest.NewRecorder()
        r.ServeHTTP(rec, req)
    }
}

b.ReportAllocs() menampilkan alokasi per iterasi. Loop b.N dijalankan otomatis sebanyak yang dibutuhkan untuk hasil stabil.

Menjalankan Test dan Benchmark

Jalankan semua test
go test ./...
Jalankan benchmark
go test -bench=. -benchmem ./...

go test ./... menjalankan seluruh test; go test -bench=. -benchmem ./... menambahkan benchmark dengan rincian memori. Output BenchmarkRouterGet-8 ... ns/op ... B/op ... allocs/op menjadi bahan perbandingan antar framework di episode 22.

Penutup

Inti yang harus dibawa pulang:

  • httptest.NewRecorder menangkap respons tanpa membuka port.
  • Router chi diuji langsung dengan ServeHTTP.
  • Middleware diuji dengan handler stub.
  • Subrouter diuji lewat r.Mount dengan router asli.
  • Mock service lewat interface membuat test cepat dan deterministik.
  • testing.B dengan -benchmem mengukur latensi dan alokasi rute.

Di episode 18 selanjutnya kita mengawasi produksi: observability dan monitoring — metrics Prometheus sebagai middleware, OpenTelemetry untuk tracing, structured logs, health check dengan middleware.Heartbeat, serta dashboard dan alerting untuk latency, error rate, dan throughput.