Use this file to discover all available pages before exploring further.
chi is built entirely on the standard net/http interface, which means you can test every handler, middleware, and router using only Go’s built-in net/http/httptest package — no special testing framework needed. This guide walks through the core patterns chi itself uses internally and that you should use in your own services.
Use httptest.NewRecorder() and http.NewRequest() to invoke a handler function without starting a server. This is ideal for handlers that do not depend on URL parameters.
URL parameters are stored in chi’s routing context, which is normally populated by the chi router during a real request. When testing a handler in isolation (without a router), you need to inject the routing context manually.The pattern chi itself uses in mux_test.go:
url_param_test.go
package mainimport ( "context" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5")func greetHandler(w http.ResponseWriter, r *http.Request) { name := chi.URLParam(r, "name") w.Write([]byte("hi " + name))}func TestGreetHandler_WithURLParam(t *testing.T) { req, _ := http.NewRequest(http.MethodGet, "/", nil) // Build a chi routing context and inject the URL parameter manually. rctx := chi.NewRouteContext() rctx.URLParams.Add("name", "joe") // Attach the routing context to the request context. req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) w := httptest.NewRecorder() greetHandler(w, req) if w.Body.String() != "hi joe" { t.Fatalf("expected 'hi joe', got '%s'", w.Body.String()) }}
chi.NewRouteContext() returns a fresh *chi.Context. After adding params
with rctx.URLParams.Add(key, value), store it on the request context using
the exported chi.RouteCtxKey as the key.
For end-to-end tests that exercise middleware, route matching, and handler behaviour together, start an httptest.Server backed by the fully configured chi router. This mirrors the approach used throughout chi’s own mux_test.go.
Testing with httptest.NewRecorder directly on the router
You can also call router.ServeHTTP(w, r) directly on a chi router without starting a TCP server. This is slightly faster than httptest.NewServer and works well when you do not need a real URL:
recorder_router_test.go
package mainimport ( "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5")func TestRouter_WithRecorder(t *testing.T) { r := chi.NewRouter() r.Get("/hi", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("bye")) }) r.NotFound(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) w.Write([]byte("nothing here")) }) tests := []struct { path string want string status int }{ {"/hi", "bye", http.StatusOK}, {"/nothing-here", "nothing here", http.StatusNotFound}, } for _, tc := range tests { req, _ := http.NewRequest(http.MethodGet, tc.path, nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != tc.status { t.Errorf("path %s: expected status %d, got %d", tc.path, tc.status, w.Code) } if w.Body.String() != tc.want { t.Errorf("path %s: expected body '%s', got '%s'", tc.path, tc.want, w.Body.String()) } }}
Test middleware by wiring it to a minimal router or by calling it directly around a stub handler. The example below tests that a custom authentication middleware returns 401 when no token is present and calls the next handler when a token is provided.
middleware_test.go
package mainimport ( "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5")// TokenAuth is a simple middleware that checks for a Bearer token.func TokenAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Authorization") if token != "Bearer secret" { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } next.ServeHTTP(w, r) })}func TestTokenAuth_Missing(t *testing.T) { r := chi.NewRouter() r.With(TokenAuth).Get("/protected", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok")) }) req, _ := http.NewRequest(http.MethodGet, "/protected", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("expected 401, got %d", w.Code) }}func TestTokenAuth_Valid(t *testing.T) { r := chi.NewRouter() r.With(TokenAuth).Get("/protected", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("ok")) }) req, _ := http.NewRequest(http.MethodGet, "/protected", nil) req.Header.Set("Authorization", "Bearer secret") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected 200, got %d", w.Code) } if w.Body.String() != "ok" { t.Errorf("expected 'ok', got '%s'", w.Body.String()) }}
Sometimes a middleware sets values on a parent context before the request reaches the router (e.g., from a framework that pre-populates context). You can pass that context into the test request with r.WithContext:
existing_context_test.go
package mainimport ( "context" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5")type ctxKey struct{ name string }func TestHandler_ExistingContext(t *testing.T) { r := chi.NewRouter() r.Get("/hi", func(w http.ResponseWriter, r *http.Request) { val, _ := r.Context().Value(ctxKey{"msg"}).(string) w.Write([]byte(val)) }) req, _ := http.NewRequest(http.MethodGet, "/hi", nil) req = req.WithContext( context.WithValue(req.Context(), ctxKey{"msg"}, "hello from context"), ) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Body.String() != "hello from context" { t.Errorf("unexpected body: %s", w.Body.String()) }}
httptest.NewServer starts a real HTTP server on a random localhost port and returns a *httptest.Server with a .URL field. Use it when you need to test with real HTTP semantics — cookies, redirects, connection pooling — or when invoking code that builds an absolute URL.
Register a custom r.NotFound(...) handler on your router, then make a
request to a path that does not match any registered route. Assert that
w.Code equals http.StatusNotFound and check the body.
Can I test method-not-allowed responses?
Yes. Register a route for GET /hi only, then make a POST /hi request.
chi will call the MethodNotAllowed handler (default or custom) and return
405. You can also register r.MethodNotAllowed(...) to customise the
response.