Go’s httptest
package provides a really simple, elegant way to test your HTTP services. It allows you to create requests to and capture the responses from anything that implements the http.Handler
interface:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
There are many ways to acheive this and each is tested slightly different. A colleague of mine recently asked for some help testing his HTTP server and I’m hoping that this post might help others test theirs, regardless of how they’ve implemented it.
I’ll provide a super simple example of each server flavour, along with an example of how to test it using the httptest
package.
Background
Before we dive in, I’ll explain the basics of the httptest
package, to highlight why it’s so useful.
An HTTP handler in Go takes two arguments, an http.ResponseWriter
, which is an interface that describes how to provide a response to the caller and a pointer to an http.Request
which contains the request URI, the request body and various other properties. Therefore, in order to make a request (at runtime or in tests), you’ll need to provide these.
Happily, the httptest
package provides functions that simplify the creation of both. Namely, httptest.NewRequest
and httptest.NewRecorder
.
// Everything required by a ResponseWriter.
resp := httptest.NewRecorder()
The call to httptest.NewRecorder
returns an httptest.ResponseRecorder
which implements the http.ResponseWriter
. This satisfies the first parameter of ServeHTTP
.
// Everything required to make a GET request with no body.
req := httptest.NewRequest(http.MethodGet, "/", nil)
The call to httptest.NewRequest
returns a bare-bones instance of an *http.Request
containing an HTTP verb (in this case “GET”), a target URL (in this case “/") which becomes the RequestURI
and a body (in this case nil
). This satisfies the second parameter of ServeHTTP
.
Remembering that everything in Go’s world of HTTP boils down to a call to ServeHTTP
with two parameters, you now have everything you’ll need to test an HTTP server.
To keep things simple and still copy/paste friendly, I’ve removed all package names and imports from the following examples. In the 3rd-party handler sections, I’m using the Echo server mux, which can be installed as follows:
$ go get -u github.com/labstack/echo/...
import (
"github.com/labstack/echo"
)
Flavours
- Built-in
http.HandleFunc
handlers - Built-in
http.ServeMux
handlers (direct) - Built-in
http.ServeMux
handlers (routing) - Built-in
http.ServeMux
handlers (embedded) - Wrapped
http.HandleFunc
handlers - 3rd-party mux handlers (direct)
- 3rd-party mux handlers (routing)
- 3rd-party mux handlers (embedded)
Built-in handlers
Go’s built-in handlers are vanilla HTTP handlers that satisfy the http.Handler
interface when a call to http.HandleFunc
is used in conjunction with a function (anonymous or otherwise) with the signature func(w http.ResponseWriter, r *http.Request)
:
Server code
func main() {
http.HandleFunc("/", hello)
log.Fatal(http.ListenAndServe(":1234", nil))
}
func hello(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusTeapot)
w.Write([]byte("hello"))
}
This is the quintessential Hello, World! HTTP server example. It exposes a single endpoint that returns the word “hello” and a status code of 418 (I’m a teapot).
Test code
func TestHello(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
resp := httptest.NewRecorder()
hello(resp, req)
if resp.Code != http.StatusTeapot {
t.Fatalf("exp %d but got %d", http.StatusTeapot, resp.Code)
}
if resp.Body.String() != "hello" {
t.Fatalf("exp %s but got %s", "hello", resp.Body.String())
}
}
In this example, we only want to test our hello
function, as that contains our business logic. We therefore make a call directly to it and bypass the HTTP routing to “/” provided by the *http.Server
(which is configured in the call to http.ListenAnServe
).
By now, the only line that might seem unfamiliar to you is the direct call to hello
with our httptest.ResponseRecorder
and *http.Request
parameters.
As the call to NewRecorder
returns a pointer to an http.ResponseRecorder
, it’ll be mutated inside the call to hello
. This is what allows us to interrogate the response after making the request.
The tests pass because we’ve configured the hello
function to return a status code of “418” and a body of “hello”; all of which is captured by the httptest.ResponseRecorder
.
Built-in http.ServerMux
direct handlers
Go’s http.ServeMux
is a multiplexer that you can configure routes and handlers against. It’s a step between calling http.HandleFunc
, which abstracts you away from the http.Handler
interface and a 3rd-party multiplexer that adds additional layers of abstraction.
Server code
func main() {
mux := http.NewServeMux()
mux.Handle("/", &handler{})
log.Fatal(http.ListenAndServe(":1234", mux))
}
type handler struct {
}
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusTeapot)
w.Write([]byte("hello"))
}
In this example, we’ve created a handler
struct which implements the http.Handler
interface because of its ServeHTTP
method receiver. With this configuration, we could add additional handlers of varying complexity, to handle different routes.
It’s worth noting that mux.Handle("/", &handler{})
is equivalent to http.Handle("/", &handler{})
, so the test will work against either, without modification.
Test code
func TestHello(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
resp := httptest.NewRecorder()
h := &handler{}
h.ServeHTTP(resp, req)
if resp.Code != http.StatusTeapot {
t.Fatalf("exp %d but got %d", http.StatusTeapot, resp.Code)
}
if resp.Body.String() != "hello" {
t.Fatalf("exp %s but got %s", "hello", resp.Body.String())
}
}
This time round we’re invoking a method receiver on *handler
, rather than calling the top-level function. Everything else remains exactly the same.
Once again, the routing logic to “/” isn’t being tested, we’re just testing the function that’ll get called after the requset has been routed. If you’d like routing to form part of your unit tests, you might want to consider using http.ServeMux
as follows.
Built-in http.ServerMux
routed handlers
Server code
func main() {
mux := newMux()
http.ListenAndServe(":1234", mux)
}
func newMux() (m *http.ServeMux) {
m = http.NewServeMux()
m.HandleFunc("/a", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
})
m.HandleFunc("/b", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("goodbye"))
})
return
}
In this example, we attach all of our handlers to the *http.ServeMux
using HandleFunc
and not Handle
. This hides the ServeHTTP
methods for the handlers themselves, allowing the test code to call the ServeHTTP
method on the *http.ServeMux
struct directly; testing our routing logic.
Test code
func TestHello(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/a", nil)
resp := httptest.NewRecorder()
m := newMux()
m.ServeHTTP(resp, req)
if resp.Body.String() != "hello" {
t.Fatalf("exp %s but got %s", "hello", resp.Body.String())
}
}
This time, I’ve bound two handlers, one for route “/a” and another for “/b”. The test will pass, as the handler configured for route “/a” returns hello but change the request’s target URI to “/b” and you’ll receive the following error:
--- FAIL: TestHello (0.00s)
main_test.go:19: exp hello but got goodbye
Our routing is being tested!
Built-in http.ServerMux
embedded handlers
In your production code, you’re more likely to want to hide the *http.ServeMux
along with other dependencies in a struct of your own. It’s easy to adapt the Built-in http.ServeMux
handlers (routing) example to hide the *http.ServeMux
. There are a couple of ways you could do this but the easiest way is to use embedding:
Server code
In this example, we declare a mux
struct and “embed” the *http.ServeMux
, which automatically exposes the ServeHTTP
method on our mux
struct and the tests pass without modification.
func main() {
mux := newMux()
http.ListenAndServe(":1234", mux)
}
type mux struct {
*http.ServeMux
}
func newMux() (m *mux) {
m = &mux{
http.NewServeMux(),
}
m.HandleFunc("/a", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
})
m.HandleFunc("/b", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("goodbye"))
})
return
}
Wrapped built-in handlers
The http.HandlerFunc
type reduces a web request to a simple function call. Functions are first-class citizens in Go, meaning you can call one http.HandlerFunc
from another. The preceding http.HandlerFunc
is referred to as “middleware”.
Go’s support for closures gives you the added benefit of closing over variables and keeping them within the scope of your final handler. This is useful for passing around loggers and database sessions etc.
In the following example, I create a logging handler and chain it onto my hello
handler:
Server code
func main() {
l := log.New(os.Stdout, "", 0)
http.HandleFunc("/", withLogging(hello, l))
http.ListenAndServe(":1234", nil)
}
func hello(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
}
func withLogging(h http.HandlerFunc, l *log.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l.Println("before")
defer l.Println("after")
h(w, r)
}
}
When the endpoint is hit, we’ll execute our logging middleware first, then our hello
handler.
Test code
func TestHello(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
resp := httptest.NewRecorder()
buf := &bytes.Buffer{}
logger := log.New(buf, "", 0)
middleware := withLogging(hello, logger)
middleware(resp, req)
if resp.Body.String() != "hello" {
t.Fatalf("exp %s but got %s", "hello", resp.Body.String())
}
output := buf.String()
if output != "before\nafter\n" {
t.Fatalf("exp %s got %s", "before\nafter\n", output)
}
}
In the test, we create a *log.Logger
and set its io.Writer
to be a *bytes.Buffer
, meaning we can interrogate whatever’s logged bye our middleware. We wrap the hello
handler in the logging middleware and make the request via the logging middleware, just like we would with our hello
handler directly.
The end-user sees “hello”, just as they did before and we’re able to call the String()
method on the logger’s writer to assert that our logging middleware wrote the expected log lines.
3rd-party server mux (direct)
Every server mux is different and will require a different approach to testing. Under the covers however, everything is still just ServeHTTP
, so the tests will look familiar.
I’m a big fan of the Echo framework. Its API made sense to me from the get go and it has become my go-to server mux for web services. In the case of Echo, handlers are similar to those expected by the stdlib’s http.HandleFunc
function except for the fact that they wrap the *http.Request
and http.ResponseWriter
into an echo.Context
. This behaviour is shared by most 3rd party multiplexers.
Server code
func main() {
e := echo.New()
e.GET("/", hello)
e.Start(":1234")
}
func hello(c echo.Context) (err error) {
return c.String(http.StatusTeapot, "hello")
}
Test code
func TestHello(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
resp := httptest.NewRecorder()
e := echo.New()
c := e.NewContext(req, resp)
if err := hello(c); err != nil {
t.Fatalf("exp not error not %v", err)
}
if resp.Code != http.StatusTeapot {
t.Fatalf("exp %d but got %d", http.StatusTeapot, resp.Code)
}
if resp.Body.String() != "hello" {
t.Fatalf("exp %s but got %s", "hello", resp.Body.String())
}
}
In this example, the only deviation from standard handler testing is the creation of the wrapping context. Contexts in the Echo framework are pooled in a sync.Pool
and created with the very same NewContext
method that we’re using in the test, so it’s a great way to test Echo’s runtime context creation beaviour too.
Like the majority of tests in this post, this test bypasses routing and shows how to test just the handler we care about. The following example tests Echo’s routing behaviour.
3rd-party server mux (routing)
In order to test routing with the Echo mux, we’ll need to be able to call ServeHTTP
on *echo.Echo
in our test. To do this, I’ve moved its creation into a method called newMux
, which performs all of the route configuration and provides something for us to call ServeHTTP
on.
Server code
func main() {
e := newMux()
e.Start(":1234")
}
func newMux() (e *echo.Echo) {
e = echo.New()
e.GET("/a", hello)
e.GET("/b", goodbye)
return
}
func hello(c echo.Context) (err error) {
return c.String(http.StatusTeapot, "hello")
}
func goodbye(c echo.Context) (err error) {
return c.String(http.StatusTeapot, "goodbye")
}
Test code
func TestHello(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/a", nil)
resp := httptest.NewRecorder()
mux := newMux()
mux.ServeHTTP(resp, req)
if resp.Code != http.StatusTeapot {
t.Fatalf("exp %d but got %d", http.StatusTeapot, resp.Code)
}
if resp.Body.String() != "hello" {
t.Fatalf("exp %s but got %s", "hello", resp.Body.String())
}
}
3rd-party server mux (embedded)
Server code
In your production code, you’re more likely to want to hide the 3rd-party server mux along with other dependencies in a struct of your own.
Testing an embedded Echo mux is just as easy as testing a naked one, you just have to get at its ServeHTTP
method. I achieve this by exposing it via a method receiver on my server
struct:
func main() {
s := newServer()
s.start(":1234")
}
type server struct {
router *echo.Echo
}
func newServer() (s *server) {
s = &server{}
s.router = echo.New()
s.router.GET("/a", s.hello)
s.router.GET("/b", s.goodbye)
return
}
func (s *server) start(addr string) (err error) {
return s.router.Start(addr)
}
func (s *server) hello(c echo.Context) (err error) {
return c.String(http.StatusTeapot, "hello")
}
func (s *server) goodbye(c echo.Context) (err error) {
return c.String(http.StatusTeapot, "goodbye")
}
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.router.ServeHTTP(w, r)
}
It’s worth noting that I could have called my ServeHTTP
method anything, as it’s not essential for the tests. However, it does allows us to use server
directly in a http.Handler
and semantically, it’s clear to others what’s happening so there are definitely benefits.
Test code
func TestHello(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/a", nil)
resp := httptest.NewRecorder()
s := newServer()
s.ServeHTTP(resp, req)
if resp.Code != http.StatusTeapot {
t.Fatalf("exp %d but got %d", http.StatusTeapot, resp.Code)
}
if resp.Body.String() != "hello" {
t.Fatalf("exp %s but got %s", "hello", resp.Body.String())
}
}
As with the Built-in http.ServeMux
routed handlers tests, I’ve created two endpoints, one that returns “hello” and one that returns “goodbye” to highlight that we’re testing the routing logic as well. Make a change to target “/b” instead of “/a” and you’ll get an error.