This episode makes sure the application doesn't break as it grows: unit-testing handlers with httptest, testing middleware and subrouters, and mocking services. You will also learn to benchmark with testing.B to measure route allocations and latency.

A good router means nothing if it can't be tested quickly and reliably. Episode 17 shows a major advantage of chi's design: because the router is just an http.Handler, it can be tested directly with httptest — no open port, no waiting for a database, no complicated setup. You'll write tests for handlers, middleware, and subrouters, then close with benchmarks to measure route performance — comparing allocations and latency objectively.
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) creates a fake request, and r.ServeHTTP(rec, req) runs the entire middleware stack without a real server. Results are read from rec.Code and 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") works normally because the route context is built by the router during 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) wraps a stub with the middleware and calls it directly — you don't need a full router to prove a middleware works.
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()) uses the same resource router as production (episode 5), so the test reflects real behavior including subrouter middleware.
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{} replaces the database implementation with fixed data. Handler tests are now fast, deterministic, and free of external dependencies.
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() displays allocations per iteration. The b.N loop runs automatically as many times as needed for stable results.
go test ./...go test -bench=. -benchmem ./...go test ./... runs all tests; go test -bench=. -benchmem ./... adds benchmarks with memory details. The BenchmarkRouterGet-8 ... ns/op ... B/op ... allocs/op output becomes comparison material between frameworks in episode 22.
Key takeaways:
httptest.NewRecorder captures responses without opening a port.ServeHTTP.r.Mount with the real router.testing.B with -benchmem measures route latency and allocations.In the next episode 18 we watch over production: observability and monitoring — Prometheus metrics as middleware, OpenTelemetry for tracing, structured logs, health checks with middleware.Heartbeat, and dashboards and alerting for latency, error rate, and throughput.