DEV Community

Nicolas Barbosa
Nicolas Barbosa

Posted on

7 2

Testing HTTP handlers in Go

Introduction

In this example, I will create unit tests using the httptest library and we will see how to create test coverage for http handlers.

How to test http handlers in Go?

So we can test any handler http, we need a structure to be able to store the handler response, such as http status code, headers, body, etc.
The package httptest provide the structure httptest.ResponseRecorder, that's all we need to store the handler response.

// NewRecorder returns an initialized ResponseRecorder.
func NewRecorder() *ResponseRecorder {
    return &ResponseRecorder{
        HeaderMap: make(http.Header),
        Body:      new(bytes.Buffer),
        Code:      200,
    }
}
Enter fullscreen mode Exit fullscreen mode

The sample API

In our sample API, we have a handler called /check-is-prime, which checks if a number is prime.

The code:

const (
apiAddress = ":8081"
)
func main() {
mux := setupMux()
http.ListenAndServe(apiAddress, mux)
}
func setupMux() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/check-is-prime", isPrimeHandler)
return mux
}
func isPrimeHandler(w http.ResponseWriter, r *http.Request) {
number := r.URL.Query().Get("number")
n, err := strconv.Atoi(number)
if err != nil {
http.Error(w, "invalid number", http.StatusBadRequest)
return
}
fmt.Fprint(w, strconv.FormatBool(isPrime(int64(n))))
}
func isPrime(n int64) bool {
return big.NewInt(n).ProbablyPrime(0)
}
view raw example_api.go hosted with ❤ by GitHub

The first test

The logic of the tests is very simple, for each test we build the HTTP request and then compare the response.

func Test_IsPrimeHandler(t *testing.T) {
handler := setupMux()
type args struct {
req *http.Request
}
tests := []struct {
name string
args func(t *testing.T) args
wantCode int
wantBody string
}{
{
name: "must return http.StatusBadRequest to invalid number",
args: func(*testing.T) args {
req, err := http.NewRequest("GET", "/check-is-prime", nil)
if err != nil {
t.Fatalf("fail to create request: %s", err.Error())
}
q := req.URL.Query()
q.Add("number", "not_number")
req.URL.RawQuery = q.Encode()
return args{
req: req,
}
},
wantCode: http.StatusBadRequest,
wantBody: "invalid number\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tArgs := tt.args(t)
resp := httptest.NewRecorder()
handler.ServeHTTP(resp, tArgs.req)
if resp.Result().StatusCode != tt.wantCode {
t.Fatalf("the status code should be [%d] but received [%d]", resp.Result().StatusCode, tt.wantCode)
}
if resp.Body.String() != tt.wantBody {
t.Fatalf("the response body should be [%s] but received [%s]", resp.Body.String(), tt.wantBody)
}
})
}
}

Adding more tests

Using table design in tests, its easy to add more cases:

{
    name: "must return http.StatusOk and true to prime number (7)",
    args: func(*testing.T) args {
        req, err := http.NewRequest("GET", "/check-is-prime", nil)
        if err != nil {
            t.Fatalf("fail to create request: %s", err.Error())
        }

        q := req.URL.Query()
        q.Add("number", "7")
        req.URL.RawQuery = q.Encode()

        return args{
            req: req,
        }
    },
    wantCode: http.StatusOK,
    wantBody: "true",
},
{
    name: "must return http.StatusOk and false because number (1) is not prime",
    args: func(*testing.T) args {
        req, err := http.NewRequest("GET", "/check-is-prime", nil)
        if err != nil {
            t.Fatalf("fail to create request: %s", err.Error())
        }

        q := req.URL.Query()
        q.Add("number", "1")
        req.URL.RawQuery = q.Encode()

        return args{
            req: req,
        }
    },
    wantCode: http.StatusOK,
    wantBody: "false",
},
Enter fullscreen mode Exit fullscreen mode

Complete code:

func Test_IsPrimeHandler(t *testing.T) {
handler := setupMux()
type args struct {
req *http.Request
}
tests := []struct {
name string
args func(t *testing.T) args
wantCode int
wantBody string
}{
{
name: "must return http.StatusBadRequest to invalid number",
args: func(*testing.T) args {
req, err := http.NewRequest("GET", "/check-is-prime", nil)
if err != nil {
t.Fatalf("fail to create request: %s", err.Error())
}
q := req.URL.Query()
q.Add("number", "not_number")
req.URL.RawQuery = q.Encode()
return args{
req: req,
}
},
wantCode: http.StatusBadRequest,
wantBody: "invalid number\n",
},
{
name: "must return http.StatusOk and true to prime number (7)",
args: func(*testing.T) args {
req, err := http.NewRequest("GET", "/check-is-prime", nil)
if err != nil {
t.Fatalf("fail to create request: %s", err.Error())
}
q := req.URL.Query()
q.Add("number", "7")
req.URL.RawQuery = q.Encode()
return args{
req: req,
}
},
wantCode: http.StatusOK,
wantBody: "true",
},
{
name: "must return http.StatusOk and false because number (1) is not prime",
args: func(*testing.T) args {
req, err := http.NewRequest("GET", "/check-is-prime", nil)
if err != nil {
t.Fatalf("fail to create request: %s", err.Error())
}
q := req.URL.Query()
q.Add("number", "1")
req.URL.RawQuery = q.Encode()
return args{
req: req,
}
},
wantCode: http.StatusOK,
wantBody: "false",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tArgs := tt.args(t)
resp := httptest.NewRecorder()
handler.ServeHTTP(resp, tArgs.req)
if resp.Result().StatusCode != tt.wantCode {
t.Fatalf("the status code should be [%d] but received [%d]", resp.Result().StatusCode, tt.wantCode)
}
if resp.Body.String() != tt.wantBody {
t.Fatalf("the response body should be [%s] but received [%s]", resp.Body.String(), tt.wantBody)
}
})
}
}

Conclusion

Have these utilities in the Go standard library, helps a lot when creating and maintaining our tests. We saw in practice with the package httptest.

Another important point is to isolate mux creation, this is necessary to call handlers in tests.

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay