The mux.Vars Problem


We’ve begun to use github.com/gorilla/mux at work as a router for many of our services. I ran into a snag when writing some unit tests, and learned a bit about the design of mux in the process.

A quick detour

When I say “unit tests”, I mean this:

… a software development process in which the smallest testable parts of an application, called units, are individually and independently scrutinized for proper operation. definition from techtarget.com

More directly, I mean calling a single function with inputs formed specifically to test the behavior of that function. When I “unit test” handler funcs, I call the handler directly; I don’t want to involve other parts of the system (like a mux.Router).

The mux.Vars problem

github.com/gorilla/mux is a Golang HTTP router. Among other features, it provides a nice way to extract parameters from a request’s path, using mux.Vars which is a map[string]string. However, the way it works makes it impossible to unit test handlers (in a pure sense).

Here’s an example:

router.Methods("GET").Path("/posts/{year}/{month}/{day}").HandlerFunc(ShowPost)

This sets up a route for GET /posts/{year}/{month}/{day} where the year, month, and day segments of the request path are parsed as variables in mux.Vars.

Then, in the ShowPost handler, the year, month, and day variables are accessed like this:

vars := mux.Vars(req)

year  := vars["year"]
month := vars["month"]
day   := vars["day"]

Let’s get a little insight into how this works. Here’s the definition of the mux.Vars func:

// Vars returns the route variables for the current request, if any.
func Vars(r *http.Request) map[string]string {
	if rv := context.Get(r, varsKey); rv != nil {
		return rv.(map[string]string)
	}
	return nil
}

This is using github.com/gorilla/context, which maintains a global map[*http.Request]map[interface{}]interface{}. When mux is routing a request, it parses the parameters from the request path, forms the map[string]string for Vars, and does context.Set(req, varsKey, vars), which stashes vars in the global map, using req and then varsKey as keys.

In the func above, those vars are fetched with context.Get(r, varsKey) as a map[interface{}]interface{}, and then type-asserted back into a map[string]string.

When unit-testing a handler, if the handler uses mux.Vars to get at request params (as we did in the example above), then we’ll need to craft a map[string]string to use with the test. We then would want to set that map in the request’s context with something like context.Set(testReq, someKey, testMap). So… where do we get someKey?

Back in that same file in github.com/gorilla/mux, here’s the definition of varsKey:

type contextKey int

const (
	varsKey contextKey = iota
	routeKey
)

varsKey is of type contextKey, which is unexported; that means it’s not accessible outside of the github.com/gorilla/mux package. This means we can’t craft the right type of key to set our test vars in the context of a test request. That means we have to have github.com/gorilla/mux process the test request. So we have to create a mux.Router wired up with the handler under test, and call ServeHTTP on that router. That’s closer to integration testing than pure unit testing of the handler.

This all would be solved if either the varsKey var or the contextKey type were exported. I’m consdering submitting a pull request to github.com/gorilla/mux to do just that; if I do, I’ll link to it here.