rss twitter gitlab github linkedin linkedin instagram
Microservices in Go: REST APIs (Part 2) - Testing
Apr 25, 2021

This post is part 2 in a series:



Disclaimer: This post includes Amazon affiliate links. If you click on one of them and you make a purchase I’ll earn a commission. Please notice your final price is not affected at all by using those links.


Implementing tests

When we implemented HTTP Handlers last time we mentioned a few difficult things to do when using the standard library, like pattern matching or nested resources, testing handlers in Go on the other hand requires no third party packages, this is because of net/http/httptest which is already included in the standard library.

Using net/http/httptest in the simplest form works like this:

package main

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
)

func main() {
	handler := func(w http.ResponseWriter, r *http.Request) {
		io.WriteString(w, "<html><body>net/http/httptest</body></html>")
	}

	req := httptest.NewRequest("GET", "http://test.com/foo", nil)
	w := httptest.NewRecorder()
	handler(w, req)

	resp := w.Result()
	body, _ := io.ReadAll(resp.Body) // XXX: Explicitly ignoring errors

	fmt.Printf("%d %s %s", resp.StatusCode, resp.Header.Get("Content-Type"), string(body))
}

Where a httptest.ResponseRecorder acts as a writer when requesting a handler, this argument will in the end record the results being written to and it will allow us to test we are trying to implement.

Structuring HTTP tests

The code used for this post is available on Github.

Because of the way we have been implementing our handlers already, the structure I like to follow when implementing tests is the following:

func Test<Name>_<HTTPVerb>(t *testing.T) {
	t.Parallel()

	type output struct {
		expectedStatus int
		expected       interface{}
		target         interface{}
	}

	tests := []struct {
		name   string
		setup  func(*resttesting.Fake<Task>Service)
		input  []byte
		output output
	}{
		{
			// ...
		},
	}

	for _, tt := range tests {
		tt := tt

		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			router := mux.NewRouter()
			svc := &resttesting.Fake<Task>Service{}
			tt.setup(svc)

			rest.New<Task>Handler(svc).Register(router)

			//-

			res := doRequest(router,
				httptest.NewRequest(http.MethodPost, "/<name>", bytes.NewReader(tt.input)))

			//-

			assertResponse(t, res, test{tt.output.expected, tt.output.target})

			if tt.output.expectedStatus != res.StatusCode {
				t.Fatalf("expected code %d, actual %d", tt.output.expectedStatus, res.StatusCode)
			}
		})
	}
}

The idea is:

  1. Define an output struct type, meant to represent the data returned back in the response, this means the status code, response and the target type to use for unmarshaling.
  2. I like defining a helper test function, something like assertResponse, to compare the body results to an expected unmarshaled value.
  3. The tests table will define tests using the usual input and output as well as a new setup function that will initialize our mock type,
  4. Finally our actual test will connect all the dots and call the initializer, do the request and compare the results.

Feel free to review the implement tests in the repository for more concrete details.

Conclusion

Implementing tests for HTTP Handlers requires a few steps, is easier than implementing handler and in practice it should not require third party packages to accomplish what we are trying to confirm, if anything perhaps using a package for equality like github.com/google/go-cmp should be more than enough.

In the next post I will cover implementing a custom type that represents the type used for a field, the idea is to improve our API and make it more user friendly way for our users.

If you’re looking to sink your teeth into more REST and Web Programming I recommend the following books:


Back to posts