Currently working on a prototype microservice and to get started quickly I
decided to use gorilla/mux as my router
because of its low barrier of entry and plenty of resources out there on it.
One thing that has bothered me before when I have used mux
was how to go about
testing the HTTP handlers. Discovered a nice way to do it.
tl;dr Checkout this gist.
There are many ways to go about this, but here are some requirements that helped to shape the design:
- I wanted to pass in any dependencies directly to the handler. I like the simplicity of this and it avoids things like global structs or a single massive struct that contains any possible dependency you might use across your handlers.
- I wanted to test the routing to the handler as well. While you can test the handler in isolation, I like the idea of knowing that the route config works as expected.
In order to satisfy the above, we need to wire up a mux
router with our route
and handler in each test. Two problems can arise while testing this way:
- The route definition can become duplicated in the test, which might lead to the route becoming stale. You are also not actually testing your real route.
- Avoiding duplication can also lead to another problem: the router with all the routes is usually built all at once. This can become a problem at testing time if you have a wide variety of dependencies across all of your handlers. You would have to build out those dependencies in each test in order to just route to a single handler.
To avoid duplication of the route and to avoid building the router with all the routes and handlers, I came up with this really simple solution:
package main
import (
"net/http"
"github.com/gorilla/mux"
)
// Handler is responsible for defining a HTTP request route and corresponding handler.
type Handler struct {
// Receives a route to modify, like adding path, methods, etc.
Route func(r *mux.Route)
// HTTP Handler
Func http.HandlerFunc
}
// AddRoute adds the handler's route the to the router.
func (h Handler) AddRoute(r *mux.Router) {
h.Route(r.NewRoute().HandlerFunc(h.Func))
}
The above defines a Handler
struct with two fields:
Handler.Route
: this is used to define the route for the handler.Handler.Func
: this is used to define the actual HTTP handler.
In addition, the Handler
struct has the AddRoute
method to attach the
handler’s route to the router.
Here is how you define a new handler:
package main
import (
"log"
"net/http"
"github.com/gorilla/mux"
)
func Greeter(prefix string) Handler {
return Handler{
Route: func(r *mux.Route) {
r.Path("/greet/{name}").Methods("GET")
},
Func: func(w http.ResponseWriter, r *http.Request) {
name, ok := mux.Vars(r)["name"]
if !ok || name == "" {
name = "Champ"
}
_, err := w.Write([]byte(prefix + " " + name + "!"))
if err != nil {
log.Printf("Failed to write to response: %s\n", err)
}
},
}
}
The above defines a Greeter
function that returns the Handler
struct. It
accepts a single dependency, which is just a string. With a little imagination
though, the dependency could be something more complicated, like database
connection, application service, etc. Then it fills out the Handler
by
providing the Route
and Func
logic.
Here is how it looks to actually use Greeter
:
package main
import (
"log"
"net/http"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
complexDependency := "Hello" // ;)
Greeter(complexDependency).AddRoute(r)
log.Fatal(http.ListenAndServe(":8000", r))
}
Simple enough. Just create the router, build Greeter
and add its route to the
router. Lastly, start the server.
And finally, this is how it looks to test Greeter
:
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/mux"
)
func TestGreeter(t *testing.T) {
w := httptest.NewRecorder()
r := mux.NewRouter()
Greeter("Hello").AddRoute(r)
r.ServeHTTP(w, httptest.NewRequest("GET", "/greet/Hodor", nil))
if w.Code != http.StatusOK {
t.Error("Did not get expected HTTP status code, got", w.Code)
}
if w.Body.String() != "Hello Hodor!" {
t.Error("Did not get expected greeting, got", w.Body.String())
}
}
Also very straight forward, nearly identical to the actual usage. The above test
verifies the Greeter
handler’s route and the HTTP handler in isolation from
any other handler.
Conclusion
Overall, I think this is a nice approach as it bundles the route and the handler
in a single, reusable package. It is also very IDE friendly as the IDE will
auto-fill the Handler
fields with the appropriate function definitions. In
addition, it is very copy/paste friendly because the only identifier that must
be renamed is the function name (EG: Greeter
).