Go1.11 Google Cloud Platform AppEngine local unit-testing. (Part 1.)

Kirill Matyunin
4 min readDec 21, 2019

Once upon a time I’ve been given with a test task. The task was to scrape a bunch of pages using a certain python-based framework. To be modest, I did alright, I was assessed nicely with one huge remark: no unit-tests. That has been eventually a sentence — they denied my application. Who I’ve been after that? A pro? Not quite. An imposter? Perhaps closer. Regardless of the answer, lesson’s learnt. So let’s talk some unit-tests for your Go 1.11 application in Google’s AppEngine.

In my previous article I’ve come up with a hello-world application, which does next to nothing, just staying optimistic and secured by custom auth validation. Just to recap, we had only one handler to test so far:

func (s *Service) WarmupRequestHandler(w http.ResponseWriter, r *http.Request) {
s.writeResponseData(w, http.StatusOK, &Response{"OK"})
return
}

and our routes in the main.go looked like the following:

r.HandleFunc("/_ah/warmup", s.WarmupRequestHandler).Methods("GET") r.HandleFunc("/test-admin", s.WarmupRequestHandler).Methods("GET") r.HandleFunc("/test-no-admin",s.WarmupRequestHandler).Methods("GET")
r.HandleFunc("/auth",s.Auth(s.WarmupRequestHandler)).Methods("GET")

Auth is a middleware which takes an instance of http.HttpHandler function and returns it wrapped around with some logic. In our example that was the JWT (JSON Web-based token) validation. The full middleware code may be found following by the link:

The context problem

Google’s App Engine First Generation forces you to pass context along and all over the place. That wouldn’t have been an issue, if the context was a standard one, meaning an instance of context.Context. But that’s not the case — the latter should be an instance of google.golang.org/appengine context. And you need an instance of request to initialize it. The ideal solution would be to come up with a context middleware, where we have an instance of request and the context would be initialized from it. Then we’re able to use it wherever there’s a demand for.

The best place to put the context would be a request instance itself, since the latter allows you to do it with context. A bit later I’ll show you what it has to do with the unit-tests, as of now, we want to init the context from request and then call whatever handler:

func (s *Service) ContextMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := appengine.NewContext(r)
ctx, _ = appengine.Namespace(ctx, "")

next.ServeHTTP(w, r.WithContext(ctx))
}
}

Then, whenever we might have a need for context, it’s always accessible by calling r.Context() method. For example, in our auth middleware we’re logging who calls our API the following way:

log.Infof(r.Context(), "URL requested by: %s", claims.Email)

Now, as we have a context wrapper, let’s update our routes:

r.HandleFunc("/_ah/warmup", s.ContextMiddleware(s.WarmupRequestHandler)).Methods("GET")
r.HandleFunc("/test-admin", s.ContextMiddleware(s.WarmupRequestHandler)).Methods("GET")
r.HandleFunc("/test-no-admin", s.ContextMiddleware(s.WarmupRequestHandler)).Methods("GET")

As you can see, not that much has changed: we’ve just wrapped all of our handlers with s.ContextMiddleware. When it comes to our home-grown authorization validation, that would look slightly different, but not that much: we still want to init context first, then do the auth check and only then to call our handler.

r.HandleFunc("/auth", s.ContextMiddleware(s.Auth(s.WarmupRequestHandler))).Methods("GET")

All right, now all set to move on to unit-tests.

The testing

Google provides us with a testing lib: google.golang.org/appengine/aetest. It basically runs a local development server allowing you to have an access to the execution context. Previously, I’ve pointed out that you have to use appengine’s context all over the place. In tests we would deal with that testing context.

The main idea is to prepare the request, run the app under test, do the request, serve the request and then validate the response from app. In other words, you might want to call the handler itself, providing it with the writer interface and the request itself. With having that said, basic test structure would look like the following:

func TestService(t *testing.T) {
testContext, done, err := aetest.NewContext()
if err != nil {
t.Fatal(err)
}

testContext, _ = appengine.Namespace(testContext, "")
defer done()
t.Run("TEST_NAME", func(t *testing.T) {
// Do the test with testContext
})

To do the test you basically are in a need of four things: the service, the request, the testing context and http.ResponseWriter implementation. The latter is nicely represented by net/http/httptest library and could be initialized as simple as the following:

rr := httptest.NewRecorder()

The request part is as simple as that:

req, _ := http.NewRequest(http.MethodGet, "/_ah/warmup", &bytes.Buffer{})

Now, to perform the actual request we gotta call our handler, which basically means to call the ServeHTTP method with our testing context.

handlerFunc = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s.WarmupRequestHandler(w, r)
})
handlerFunc.ServeHTTP(rr, req.WithContext(testContext))

Putting it altogether:

func TestService(t *testing.T) {
testContext, done, err := aetest.NewContext()
if err != nil {
t.Fatal(err)
}

testContext, _ = appengine.Namespace(testContext, "")
defer done()
t.Run("TEST_NAME", func(t *testing.T) {
// Define the service under test
s := &Service{}
// Define the request and response recorder
req, _ := http.NewRequest(http.MethodGet, "/_ah/warmup", &bytes.Buffer{})
rr := httptest.NewRecorder()
// Define test handler and call the actual handler
handlerFunc = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s.WarmupRequestHandler(w, r)
})

handlerFunc.ServeHTTP(rr, req.WithContext(testContext))

// Status code check
if statusCode := rr.Code; statusCode != test.expectedCode {
t.Errorf("handler returned wrong status code: got %v want %v", statusCode, test.expectedCode)
}

// Body check
var actualResponse Response
json.NewDecoder(rr.Body).Decode(&actualResponse)
if actualResponse != Response{"OK"} {
t.Errorf("handler returned wrong response code: got %v want %v", actualResponse, Response{"OK"})
}
})

When it comes to testing the auth part, just make sure you wrap the handler the same way it’s done in main.go:

handlerFunc = s.Auth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s.WarmupRequestHandler(w, r)
}))
handlerFunc.ServeHTTP(rr, req.WithContext(testContext))

and don’t forget to add token header to the request

req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "TOKEN"))

The full tests code may be found up here:

I’ll discover more of a unit-testing in Part 2. Happy unit-testing!

--

--