You are on page 1of 8

4/20/2021 Testing Your (HTTP) Handlers in Go · questionable services

questionable services
Technical writings about computing infrastructure, HTTP & security.
(by Matt Silverlock)

Testing Your (HTTP) Handlers in


Go
• • •

You’re building a web (HTTP) service in Go, and you want to unit
test your handler functions. You’ve got a grip on Go’s net/http
package, but you’re not sure where to start with testing that your
handlers return the correct HTTP status codes, HTTP headers or
response bodies.
Let’s walk through how you go about this, injecting the necessary
dependencies, and mocking the rest.
A Basic Handler

We’ll start by writing a basic test: we want to make sure our handler
returns a HTTP 200 (OK) status code. This is our handler:
// handlers.go
package handlers

// e.g. http.HandleFunc("/health-check", HealthCheckHandler)


func HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
// A very simple health check.
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")

// In the future we could report back on the status of our DB, or our cache
// (e.g. Redis) by performing a simple PING, and include them in the response.
io.WriteString(w, `{"alive": true}`)
}

https://blog.questionable.services/article/testing-http-handlers-go/ 1/8
4/20/2021 Testing Your (HTTP) Handlers in Go · questionable services

And this is our test:


// handlers_test.go
package handlers

import (
"net/http"
"net/http/httptest"
"testing"
)

func TestHealthCheckHandler(t *testing.T) {


// Create a request to pass to our handler. We don't have any query parameters for
// pass 'nil' as the third parameter.
req, err := http.NewRequest("GET", "/health-check", nil)
if err != nil {
t.Fatal(err)
}

// We create a ResponseRecorder (which satisfies http.ResponseWriter) to record th


rr := httptest.NewRecorder()
handler := http.HandlerFunc(HealthCheckHandler)

// Our handlers satisfy http.Handler, so we can call their ServeHTTP method


// directly and pass in our Request and ResponseRecorder.
handler.ServeHTTP(rr, req)

// Check the status code is what we expect.


if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}

// Check the response body is what we expect.


expected := `{"alive": true}`
if rr.Body.String() != expected {
t.Errorf("handler returned unexpected body: got %v want %v",
rr.Body.String(), expected)
}
}

As you can see, Go’s testing and httptest packages make testing our
handlers extremely simple. We construct a *http.Request, a
*httptest.ResponseRecorder, and then check how our handler has

responded: status code, body, etc.


If our handler also expected speci c query parameters or looked for
certain headers, we could also test those:
// e.g. GET /api/projects?page=1&per_page=100
req, err := http.NewRequest("GET", "/api/projects",
// Note: url.Values is a map[string][]string
url.Values{"page": {"1"}, "per_page": {"100"}})
if err != nil {
https://blog.questionable.services/article/testing-http-handlers-go/ 2/8
4/20/2021 Testing Your (HTTP) Handlers in Go · questionable services
t.Fatal(err)
}

// Our handler might also expect an API key.


req.Header.Set("Authorization", "Bearer abc123")

// Then: call handler.ServeHTTP(rr, req) like in our first example.

Further, if you want to test that a handler (or middleware) is


mutating the request in a particular way, you can de ne an
anonymous function inside your test and capture variables from
within by declaring them in the outer scope.
// Declare it outside the anonymous function
var token string
test http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
// Note: Use the assignment operator '=' and not the initialize-and-assign
// ':=' operator so we don't shadow our token variable above.
token = GetToken(r)
// We'll also set a header on the response as a trivial example of
// inspecting headers.
w.Header().Set("Content-Type", "application/json")
})

// Check the status, body, etc.

if token != expectedToken {
t.Errorf("token does not match: got %v want %v", token, expectedToken)
}

if ctype := rr.Header().Get("Content-Type"); ctype != "application/json") {


t.Errorf("content type header does not match: got %v want %v",
ctype, "application/json")
}

Tip: make strings like application/json or Content-Type package-level


constants, so you don’t have to type (or typo) them over and over. A
typo in your tests can cause unintended behaviour, becasue you’re
not testing what you think you are.
You should also make sure to test not just for success, but for
failure too: test that your handlers return errors when they should
(e.g. a HTTP 403, or a HTTP 500).
Populating context.Context in Tests

https://blog.questionable.services/article/testing-http-handlers-go/ 3/8
4/20/2021 Testing Your (HTTP) Handlers in Go · questionable services

What about when our handlers are expecting data to be passed to


them in a context.Context? How we can create a context and populate it
with (e.g.) an auth token and/or our User type?
Go 1.7 added the Request.Context() method, thus supporting
request contexts natively. We’ll use what net/http
provides to make our application compatible with as
many libraries as we might need in the future.
Note that for the below example, the standard http.Handler and
http.HandlerFunc types. Whilst these testing methods are easy enough

to ‘port’ to other routers using their own types, the best routers are
those that are compatible with Go’s existing interfaces. chi and
gorilla/mux are my picks.
func TestGetProjectsHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/api/users", nil)
if err != nil {
t.Fatal(err)
}

rr := httptest.NewRecorder()
// e.g. func GetUsersHandler(ctx context.Context, w http.ResponseWriter, r *http.R
handler := http.HandlerFunc(GetUsersHandler)

// Populate the request's context with our test data.


ctx := req.Context()
ctx = context.WithValue(ctx, "app.auth.token", "abc123")
ctx = context.WithValue(ctx, "app.user",
&YourUser{ID: "qejqjq", Email: "user@example.com"})

// Add our context to the request: note that WithContext returns a copy of
// the request, which we must assign.
req = req.WithContext(ctx)
handler.ServeHTTP(rr, req)

// Check the status code is what we expect.


if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
}

Extending this, we can also test that middleware populating the


context does so correctly:
https://blog.questionable.services/article/testing-http-handlers-go/ 4/8
4/20/2021 Testing Your (HTTP) Handlers in Go · questionable services

// e.g. middleware.go
func RequestIDMiddleware(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
// More correctly, we'd use a const key of type struct{} and a random
// crypto/rand.
ctx := context.WithValue(r.Context(), "app.req.id", "12345")

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

return http.HandlerFunc(fn)
}

// e.g. middleware_test.go
func TestPopulateContext(t *testing.T) {
req, err := http.NewRequest("GET", "/api/users", nil)
if err != nil {
t.Fatal(err)
}

testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {


if val, ok := r.Context().Value("app.req.id").(string); !ok {
t.Errorf("app.req.id not in request context: got %q", val)
}
})

rr := httptest.NewRecorder()
// func RequestIDMiddleware(h http.Handler) http.Handler
// Stores an "app.req.id" in the request context.
handler := RequestIDMiddleware(testHandler)
handler.ServeHTTP(rr, req)
}

Running go test in our package should see this pass. The inverse of
this approach is also useful - e.g. testing that admin tokens aren’t
incorrectly applied to the wrong users or contexts aren’t passing
the wrong values to wrapped handlers.
Mocking Database Calls

Our handlers expect that we pass them a datastore.ProjectStore (an


interface type) with three methods (Create, Get, Delete). We want
to stub this for testing so that we can test that our handlers
(endpoints) return the correct status codes.
You should read this Thoughtbot article and this article
from Alex Edwards if you’re looking to use interfaces to
abstract access to your database.
https://blog.questionable.services/article/testing-http-handlers-go/ 5/8
4/20/2021 Testing Your (HTTP) Handlers in Go · questionable services

// handlers_test.go
package handlers

// Throws errors on all of its methods.


type badProjectStore struct {
// This would be a concrete type that satisfies datastore.ProjectStore.
// We embed it here so that our goodProjectStub type has all the methods
// needed to satisfy datastore.ProjectStore, without having to stub out
// every method (we might not want to test all of them, or some might be
// not need to be stubbed.
*datastore.Project
}

func (ps *projectStoreStub) CreateProject(project *datastore.Project) error {


return datastore.NetworkError{errors.New("Bad connection"}
}

func (ps *projectStoreStub) GetProject(id string) (*datastore.Project, error) {


return nil, datastore.NetworkError{errors.New("Bad connection"}
}

func TestGetProjectsHandlerError(t *testing.T) {


var store datastore.ProjectStore = &badProjectStore{}

// We inject our environment into our handlers.


// Ref: http://elithrar.github.io/article/http-handler-error-handling-revisited/
env := handlers.Env{Store: store, Key: "abc"}

req, err := http.NewRequest("GET", "/api/projects", nil)


if err != nil {
t.Fatal(err)
}

rr := httptest.Recorder()
// Handler is a custom handler type that accepts an env and a http.Handler
// GetProjectsHandler here calls GetProject, and should raise a HTTP 500 if
// it fails.
handler := Handler{env, GetProjectsHandler)
handler.ServeHTTP(rr, req)

// We're now checking that our handler throws an error (a HTTP 500) when it
// should.
if status := rr.Code; status != http.StatusInternalServeError {
t.Errorf("handler returned wrong status code: got %v want %v"
rr.Code, http.StatusOK)
}

// We'll also check that it returns a JSON body with the expected error.
expected := []byte(`{"status": 500, "error": "Bad connection"}`)
if !bytes.Equals(rr.Body.Bytes(), expected) {
t.Errorf("handler returned unexpected body: got %v want %v",
rr.Body.Bytes(), expected)
}

This was a slightly more complex example—but highlights how we


might:

https://blog.questionable.services/article/testing-http-handlers-go/ 6/8
4/20/2021 Testing Your (HTTP) Handlers in Go · questionable services

Stub out our database implementation: the unit tests in package


handlers should not need a test database.

Create a stub that intentionally throws errors, so we can test


that our handlers throw the right status code (e.g. a HTTP 500)
and/or write the expected response.
How you might go about creating a ‘good’ stub that returns a
(static) *datastore.Project and test that (for example) we can
marshal it as JSON. This would catch the case where changes
to the upstream type might cause it to be incompatible with
encoding/json.

What Next?

This is by no means an exhaustive guide, but it should get you


started. If you’re stuck building a more complex example, then ask
over on the Gophers Slack community, or take a look at the
packages that import httptest via GoDoc.
Posted on 20 March 2016

Recent Posts

A Guide To Writing Logging Middleware in Go

Admission Control: A helpful micro-framework for Kubernetes

Building Go Projects on CircleCI

Connecting to a Coral TPU Dev Board with Windows

https://blog.questionable.services/article/testing-http-handlers-go/ 7/8
4/20/2021 Testing Your (HTTP) Handlers in Go · questionable services

© 2020 Matt Silverlock | His photo journal | Code snippets are MIT licensed | Built with Jekyll

https://blog.questionable.services/article/testing-http-handlers-go/ 8/8

You might also like