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:
- 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. - I like defining a helper test function, something like
assertResponse
, to compare the body results to an expected unmarshaled value. - The tests table will define tests using the usual
input
andoutput
as well as a newsetup
function that will initialize our mock type, - 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.
Parting words
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.
Recommended Reading
If you're looking to sink your teeth into more REST and Web Programming I recommend the following books:
Top comments (0)