Golang httptest Example
Go (Golang) struck me when I first learned about its HTTP testing package. In other programming languages I have been used to have to do a lot of work for testing HTTP servers. In Go (Golang) this isn’t the case. There is an amazing testing package that can be used right away from the standard library and it’s extremely easy to understand. Let’s have a look at how you can use the httptest package and build better end-to-end and http tests for your Go http services.
The idea behind the httptest package is that it’ pretty easy to mock an HTTP server or to mock a response writer to feed into our server handlers. This makes it extremely easy to test http servers and clients.
In this article we are going to explore two specific scenarios where the httptest package can be useful
- testing Go http server handlers
- testing Go http clients
Testing Go HTTP Server Handlers
Let’s see an example of our http server in Go. Here below you can find an example of a server that takes a word and returns the upper case version of the same.
package main
import (
"fmt"
"log"
"net/http"
"net/url"
"strings"
)
// Req: http://localhost:1234/upper?word=abc
// Res: ABC
func upperCaseHandler(w http.ResponseWriter, r *http.Request) {
query, err := url.ParseQuery(r.URL.RawQuery)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "invalid request")
return
}
word := query.Get("word")
if len(word) == 0 {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "missing word")
return
}
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, strings.ToUpper(word))
}
func main() {
http.HandleFunc("/upper", upperCaseHandler)
log.Fatal(http.ListenAndServe(":1234", nil))
}
In this scenario we want to test the upperCaseHandler
logic that is used for our Go server. We will need two things to test that:
- http.Request that can be created using httptest.NewRequest exported function
NewRequest returns a new incoming server Request, suitable for passing to an http.Handler for testing.
- http.ResponseWriter that can be created by using httptest.NewRecorder type which returns a httptest.ResponseRecorder
ResponseRecorder is an implementation of http.ResponseWriter that records its mutations for later inspection in tests.
Using httptest.ResponseRecorder
The httptest.ResponseRecorder is an implementation of http.ResponseWriter and can be used to be passed into our server handler, record all the data that the handler will write to the response and return the data written afterwards. Let’s see how that works in the example test below.
package main
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
)
func TestUpperCaseHandler(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/upper?word=abc", nil)
w := httptest.NewRecorder()
upperCaseHandler(w, req)
res := w.Result()
defer res.Body.Close()
data, err := ioutil.ReadAll(res.Body)
if err != nil {
t.Errorf("expected error to be nil got %v", err)
}
if string(data) != "ABC" {
t.Errorf("expected ABC got %v", string(data))
}
}
This will allows us to test the handler response by inspecting the ResponseRecorder output returned by the Result method
func (rw *ResponseRecorder) Result() *http.Response
Result returns the response generated by the handler.
The returned Response will have at least its StatusCode, Header, Body, and optionally Trailer populated. More fields may be populated in the future, so callers should not DeepEqual the result in tests.
Testing Go HTTP Clients
Testing Go server handlers is relatively easy, especially when you want to test just the handler logic. You just need to mock http.ResponseWriter and http.Request objects in your tests. When working with a Go http client though, things are a little more complicated. The reason is that sometimes is not as easy to mock or replicate an entire server. Let’s see our example below.
package main
import (
"io/ioutil"
"net/http"
"github.com/pkg/errors"
)
type Client struct {
url string
}
func NewClient(url string) Client {
return Client{url}
}
func (c Client) UpperCase(word string) (string, error) {
res, err := http.Get(c.url + "/upper?word=" + word)
if err != nil {
return "", errors.Wrap(err, "unable to complete Get request")
}
defer res.Body.Close()
out, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", errors.Wrap(err, "unable to read response data")
}
return string(out), nil
}
Our Go client requires a base URL which represents a remote server base URL. It then calls the method /upper
with a given input word and returns the result to the caller as a string, or an error in case the request did not succeed. In order to test this code we will need to mock the entire server logic or at least the logic that is responsible for a given request path, like in our case the /upper
path. With the httptest package we can mock the entire server by initialising a local server listening on the loopback interface which will return anything we want.
Using httptest.Server
The type we are going to use is the httptest.Server type by calling the httptest.NewServer function.
A Server is an HTTP server listening on a system-chosen port on the local loopback interface, for use in end-to-end HTTP tests.
func NewServer(handler http.Handler) *Server
NewServer starts and returns a new Server. The caller should call Close when finished, to shut it down.
Let’s see how we can do that in the example below.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestClientUpperCase(t *testing.T) {
expected := "dummy data"
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, expected)
}))
defer svr.Close()
c := NewClient(svr.URL)
res, err := c.UpperCase("anything")
if err != nil {
t.Errorf("expected err to be nil got %v", err)
}
// res: expected\r\n
// due to the http protocol cleanup response
res = strings.TrimSpace(res)
if res != expected {
t.Errorf("expected res to be %s got %s", expected, res)
}
}
In the example above we just created a new mock server using the httptest.NewServer function, assigned it a custom mock handler that returns always the same data and used the server URL as our client’s server URL. This allows us to mock and instruct the server to return anything we want for testing purposes.
Checkout this example source code on the Go Playground https://play.golang.org/p/xQ6xRbyvOJQ