Testing HTTP handler with or without server?

Hello there,

I’m confused about testing HTTP handlers. I’ve seen two approaches and I’d like to learn what are the pros and cons of each.

For instance, having the following custom server:

package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"net/http"
	"time"
)

const maxTimeout = 5 * time.Second

var server = &http.Server{}

func main() {
	server := setUpServer()

	fmt.Println("Server running...")

	err := server.ListenAndServe()
	if err != nil {
		log.Panic(err)
	}
}

func setUpServer() *http.Server {
	mux := http.NewServeMux()

	mux.Handle("/slow", http.TimeoutHandler(http.HandlerFunc(slowHandler), maxTimeout, "timeout"))

	server.Addr = ":4040"
	server.Handler = mux

	return server
}

func slowHandler(w http.ResponseWriter, r *http.Request) {
	data := slowOperation(r.Context())

	io.WriteString(w, data+"\n")
}

func slowOperation(ctx context.Context) string {
	fmt.Println("Working on it...")

	select {
	case <-ctx.Done():
		fmt.Println("Operation timed out.")

		return ""
	case <-time.After(10 * time.Second):
		return "data"
	}
}

A handler could be tested using the custom server like:

package main

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

func TestServer(t *testing.T) {
	t.Run("slowHandler should return 503 if request timeouts", func(t *testing.T) {
		server := setUpServer()
		w := httptest.NewRecorder()
		r := httptest.NewRequest(http.MethodGet, "/slow", nil)

		server.Handler.ServeHTTP(w, r) // Using the custom server

		expected := http.StatusServiceUnavailable
		got := w.Result().StatusCode

		if got != expected {
			t.Errorf("expected request to cause %v, got %v", expected, got)
		}
	})
}

But also like:

package main

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

func TestServer(t *testing.T) {
	t.Run("slowHandler should return 503 if request timeouts", func(t *testing.T) {
		w := httptest.NewRecorder()
		r := httptest.NewRequest(http.MethodGet, "/slow", nil)

		slowHandler(w, r) // Using the handler directly

		expected := http.StatusServiceUnavailable
		got := w.Result().StatusCode

		if got != expected {
			t.Errorf("expected request to cause %v, got %v", expected, got)
		}
	})
}

In the first test we would get:

Working on it...
Operation timed out.
PASS
ok      test    5.451s

And on the second:

Working on it...
--- FAIL: TestServer (0.00s)
    --- FAIL: TestServer/slowHandler_should_return_503_if_request_timeouts (10.01s)
        main_test.go:39: expected request to cause 503, got 200
FAIL
exit status 1
FAIL    test    10.230s

So we have the first test that also tests the implementation of the server returning a correct output, and the second one that tests purely the handler failing, as it does not know about the timeout. Also, it would produce the same outcome using a totally different request like httptest.NewRequest(http.MethodDelete, "/", nil).

Should handler tests know about the server? Are there any cons if they are tested using the custom server?

1 Like

I am not an expert in the fields of http tests but I think not using a custom http server would speed up your http tests but if you also want to test your server properties(properties of http server struct which is not a lot), it could be useful in areas of where you wanna test ReadTimeout, WriteTimeout and MaxHeaderBytes for each request inside the web server.

You don’t need to use http.TimeoutHandler to timeout all endpoints, http server has default timeout parameters. I can not think of a better approach than 2nd example, because custom server approach is something when you want to test its struct properties.

Thanks, but the http.Server timeouts are not for handlers: https://stackoverflow.com/a/51259258/8966651
Also, to speed up the tests with the custom server, t.Parallel() could be used if instantiated in every test.

My question is more about if testing using the custom server is a best practice.

In my opinion, I think httptest.NewServer would be your best shot if you wanted to do this. But it doesn’t mean your setup is wrong, it is just for brevity’s sake. Most importantly you are good if you are not using both at the same time for the codebase consistency.

For the further information, I have come up with something like this. Essentially talks a bit of mocking server but doesn’t explicitly tell the pros/cons

Thanks, I’ll take a look at it!