For those people that aren’t familiar to building applications with Go and Google App Engine, the core data structure that is used whenever you need to make a request to any of the provided Google App Engine Services, is a Context.
Usually, this is created by passing in the http.Request object that is created when serving a Http request, however, when you want to automate the testing of your Http Request Handlers, you usually do something like the following:
1 2 3 4 |
req, _ := http.NewRequest("GET", "/v1/do/foo", nil) ctx = appengine.NewContext(req) // Panic: appengine: NewContext passed an unknown http.Request in api_dev.go |
You create your own Request with the values you want to test, attempt to create a Context from it…. and blammo, GAE panics because the Request wasn’t sent through the actual GAE development server.
There are a few ways to solve this problem, but this is the way I found worked best for the situation I had. I’m using Mux for doing my routing (which is a great library), which provides me with a http.Handler to server all my requests through. My first (naive) solution, was to use (another great) library Context, which enables you to attach data to the running Request. Which meant my Handler function ended up looking something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
const GAE_CONTEXT = "Context" func CreateHandler() *mux.Router { r := mux.NewRouter() s := r.PathPrefix("/v1").Subrouter() s.HandleFunc("/do/foo", f(fooHandler)) return r } func init() { http.Handle("/", CreateHandler()) } func fooHandler(w http.ResponseWriter, r *http.Request) { var ctx appengine.Context c, ok := context.GetOk(r, GAE_CONTEXT) if ok { ctx = c.(appengine.Context) } else { ctx = appengine.NewContext(r) } // ... do other things ... } |
In my my tests I could create a Context
with aetest, which creates a test Context for you, and attach it to the request for my Handler function to find along the way.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function TestFoo(t *testing.Test) { ctx, err := aetest.NewContext(nil) if err != nil { panic(err.Error()) } defer ctx.Close() Convey("When you want to do foo", t, func() { r := CreateHandler() record := httptest.NewRecorder() req, err := http.NewRequest("GET", "/v1/do/foo", nil) So(err, c.ShouldBeNil) context.Set(req, GAE_CONTEXT, ctx) Convey("It should return a 200 response", func() { r.ServeHTTP(record, req) So(record.Code, ShouldEqual, 200) }) }) } |
This didn’t feel like a good solution, and would mean that each of my http handler functions would be peppered with this boilerplate check to see there was a context or not, which I wasn’t happy with. As looked at before, I could have wrapped Mux with my a custom http.Handler, which would have worked, but given my recent proclivity for functional programming, I leaned more towards solving this problem by manipulating functions, than creating objects with state.
The first thing I did was define three types, to make writing out my higher order functions easier:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/* Standard http handler */ type HandlerFunc func(w http.ResponseWriter, r *http.Request) /* Our appengine.Context http handler */ type ContextHandlerFunc func(ctx appengine.Context, w http.ResponseWriter, r *http.Request) /* Higher order function for changing a HandlerFunc to a ContextHandlerFunc, usually creating the appengine.Context along the way. */ type ContextHandlerToHandlerHOF func(f ContextHandlerFunc) HandlerFunc |
The first type HandleFunc
is simply a convenience type for our usual http Handler function signature. The second type ContextHandlerFunc
is a type that has the function signature of what I want my http Handlers to look like when I magically have an appengine.Context
available. Finally I have a ContextHandlerToHandlerHOF
, which gives me the the function signature that I will need to take in a ContextHandlerFunc
and convert it into a HandleFunc
, so that it can be used with regular http routing APIs.
Therefore, for my application code, I have this function below, which takes in the ContextHandlerFunc
, and returns a function that matches the HandleFunc
signature, which, when invoked, will create a new appengine.Context
and pass it through to my ContextHandlerFunc
.
1 2 3 4 5 6 7 8 9 |
/* Creates a new Context and uses it when calling f */ func ContextHanderToHttpHandler(f ContextHandlerFunc) HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := appengine.NewContext(r) f(ctx, w, r) } } |
I then have a second function called CreateHandler
. It’s job it to create the mux.Router
. As an argument it takes in a ContextHandlerToHandlerHOF
, whose job it is to make the conversion to a standard HandleFunc()
format. This means we can change how the appengine.Context
gets created by passing in different ContextHandlerToHandlerHOF
implementations. In this case, our init()
function uses the one we want to use for our production code, that we defined above.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
/* Creates my mux.Router. Uses f to convert ContextHandlerFunc's to HandlerFuncs. */ func CreateHandler(f HandlerToContextHandlerHOF) *mux.Router { r := mux.NewRouter() s := r.PathPrefix("/v1").Subrouter() s.HandleFunc("/do/foo", f(fooHandler)) return r } func init() { http.Handle("/", CreateHandler(ContextHanderToHttpHandler)) } func fooHandler(ctx appengine.Context, w http.ResponseWriter, r *http.Request) { // ... do other things ... } |
This means that for my tests, I have to get a bit more creative, because I need access to my aetest.Context
outside of when I create my handler, mainly because in my tests it’s very important to Close()
it after you are done.
So below, you can see CreateContextHandlerToHttpHandler
, which creates a ContextHandlerToHandlerHOF
, with the appengine.Context
that is being provided, and rather than creating a new Context
like in production, it simply uses the one provided.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
func CreateContextHandlerToHttpHandler(ctx appengine.Context) ContextHandlerToHandlerHOF { return func (f ContextHandlerFunc) HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { f(ctx, w, r) } } } func TestDoFoo(t *testing.T) { ctx, err := aetest.NewContext(nil) if err != nil { panic(err.Error()) } defer ctx.Close() c.Convey("When you want to do foo", t, func() { r := CreateHandler(CreateContextHandlerToHttpHandler(ctx)) record := httptest.NewRecorder() req, err := http.NewRequest("GET", "/v1/do/foo, nil) c.So(err, c.ShouldBeNil) c.Convey("It should return a 200 response", func() { r.ServeHTTP(record, req) c.So(record.Code, c.ShouldEqual, 200) }) }) } |
Now I don’t get a panic from my local Google App Engine Development server when I run my tests, as I can easily switch out how the appengine.Context
is created, depending on what environment the code is running in.
I’ve also found I’ve been able able to also extend this approach, to use functional composition for my middleware layer as well (another post for another time). All in all, I’m very happy that Go has first class functions!
Comments
This is the post that brought it all together. Thanks!