TIL: Testing Parametrized URLs with Chi Router
Today I learned that it’s possible to slightly decouple your http handler tests from your chi router. Well sort of…
When writing an API you’ll often have a route with URL such as /api/v1/users/{id}
where you can get a single user by id. This case is supported by the chi router, but it isn’t clear how to inject this in a test without creating a router instance and setting up a test http server, this is not ideal when you’re trying to isolate tests and focus specifically on the handlers.
Under the hood the chi router works by adding data to the request context using the chi.RouteCtxKey
type, so to test our handlers that use path parameters, we need to inject that key into the request context during the test. In this case we’ll inject a user Id of a known user.
Here’s an example
func Test_ChiHandler(t *testing.t) {
// Create a test recorder
res := httptest.NewRecorder()
// Create a test request
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/admin/users/%v", targetUser.Id), nil)
// Create a chi Context object
chiCtx := chi.NewRouteContext()
// Create a new test request with the additional Chi contetx
req := r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, chiCtx))
// Add the key/value to the context.
chiCtx.URLParams.Add("id", fmt.Sprintf("%v", targetUser.Id))
// Now when you issue call your handler it will contain
// the user ID passed and your handler can pick it up
// as if the request was made through the chi router.
}
I prefer to contain this behavior in a simple function within a chimocker
package that I can import in a variety of handler tests.
package chimocker
import (
"context"
"net/http"
"github.com/go-chi/chi/v5"
)
type Params map[string]string
// WithUrlParam returns a pointer to a request object with the given URL params
// added to a new chi.Context object.
func WithUrlParam(r *http.Request, key, value string) *http.Request {
chiCtx := chi.NewRouteContext()
req := r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, chiCtx))
chiCtx.URLParams.Add(key, value)
return req
}
// WithUrlParams returns a pointer to a request object with the given URL params
// added to a new chi.Context object. for single param assignment see WithUrlParam
func WithUrlParams(r *http.Request, params Params) *http.Request {
chiCtx := chi.NewRouteContext()
req := r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, chiCtx))
for key, value := range params {
chiCtx.URLParams.Add(key, value)
}
return req
}
Unfortunately, you will still be coupled to the Chi router, however, in most cases I think this is a worthy trade off.