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.

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.
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.
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.
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.
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.
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.
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.
go test ./...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.
Inti yang harus dibawa pulang:
httptest.NewRecorder menangkap respons tanpa membuka port.ServeHTTP.r.Mount dengan router asli.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.