Advanced Testing for Golang RESTful Endpoints

Advanced Testing for Golang RESTful Endpoints

In my last post, I took a look at how to test simple REST handler functions - functions that be tested by passing mock request and response pointers, without relying on the RESTful library to do a lot of heavy lifting.

This time, I'm going to dive into the more complex endpoints that require the library to do some heavy lifting. In this case, I'm going to use go-restful, and the endpoints I'm testing come from this example in the repo.

tl;dr: the code from both this and the previous blog post is available on GitHub.

And, same as last time, a big thanks to Kourtney Barnes for helping me with this post.

Explain "Complex"

My last post focused on REST handlers that we could test by just calling the function directly, e.g.:

// this is pseudo code!
// please do not try to run this!

func TestSomeHandler(t *testing.T) {
  // create some mocks
  req := mockRequest()
  res := mockResponse()
  // calling the handler directly
  Handler(&req, &res)
  // run assertions
  if notWhatIsExpected(res) {
    t.Fatalf("test failed!")
  }
}

That's all good and plenty for simple handlers that don't rely on a pre-parser for their functionality, such as parsing path parameters.

However, once we get into more complex endpoints that do leverage path parameters, we'll need to push our request through the go-restful server so that it can do its behind-the-scenes parsing, and then we'll be able to test our functionality.

But Why?

Because while there is a field in the restful.Request struct that stores a map of the request's path parameters, that field is private:

// the following was copied from the v3.11.0 tag
// /request.go

// Request is a wrapper for a http Request that provides convenience methods
type Request struct {
    Request        *http.Request
    pathParameters map[string]string
    attributes     map[string]interface{} // for storing request-scoped values
    selectedRoute  *Route                 // is nil when no route was matched
}

The go-restful library has a private method in that same package that sets the parsed path parameters.

Since our code lives outside of that package, we can't set those values. The best we can do is use one of the getter methods to read them.

And That Means...

It means we have to test these complex endpoints by setting up a real go-restful request router that will take our request, pass it through the inner workings of go-restful, and feed it into our handler.

It's more setup overhead, and I'd be lying if this fully adhered to my definition of unit testing. But it works!

Setting Up a Test Server

For the purposes of brevity (and this will still not be brief), we're only going to look at one of the remaining endpoints that needs testing, the UpsertUser function. You can see the various permutations of this pattern put to use for testing the other methods on GitHub.

To get started, we'll need to create a restful.WebService so that we can register our handler function with a specific request path and HTTP method, including the types of content it accepts and responds with, the data it expects to receive, and any path parameters it uses. This is very similar to how we would set it up in our production code:

func TestUserResourceImpl_UpsertUser_Update(t *testing.T) {
    ws := new(restful.WebService)
    ws.Path("/users").Consumes(restful.MIME_JSON).Produces(restful.MIME_JSON)
    ws.Route(ws.PUT("/{user-id}").To(UserResource.UpsertUser).
        Doc("update a user").
        Param(ws.PathParameter("user-id", "identifier of the user").DataType("string")).
        Reads(models.User{})) // from the request
    wc := restful.NewContainer()
    wc.Add(ws)

    // ...
    // more to come...

}

Test Data

In my last post, I mentioned that I'm using a package-private variable to store some test data as a stand-in for a real database:

var (
    UserResource userResource = &userResourceImpl{}
    // normally one would use DAO (data access object)
    users map[string]models.User = map[string]models.User{
        "1": {"1", "Mario", 35},
        "2": {"2", "Luigi", 32},
        "3": {"3", "Toad", 481},
        "4": {"4", "Peach", 27},
    }
)

To test this endpoint, we'll make an update to users["1"], changing Mario's age.

Even though we have a struct that we can use for this data object, we can only pass in an io.Reader as the body parameter of httptest.NewRequest. That means that after we create this struct, we'll have to convert it to a bytes.NewBuffer, so that it fits the NewRequest function signature:

func TestUserResourceImpl_UpsertUser_Update(t *testing.T) {

    // the server setup from above...
    // ...

    mario := &models.User{ID: "1", Name: "Mario", Age: 40}
    b, err := json.Marshal(mario)
    if err != nil {
        t.Fatalf("error marshaling test model.User to json: %s", err.Error())
    }
    httpReq := httptest.NewRequest(http.MethodPut, "/users/1", bytes.NewBuffer(b))
    httpReq.Header.Set("Content-Type", restful.MIME_JSON) // very important!

    // ...
    // more to come...

}

Notice that we're setting the "Content-Type" header; this is so that go-restful's internal data parsing knows how to handle the request body that we're sending.

Remember that our handler for PUT /users/<user-id> is going to rely on that user-id path parameter, so it's super important that we use "/users/1" as the request path if that's the user we're targeting for an update.

Serving a One-Off Request

At the beginning of our test setup, we created a restful.Container, and added our restful.WebService, which includes the route handler function that we want to test. At this point in our test function, we can use our Container to respond to a single request, using its built-in ServeHTTP method:

func TestUserResourceImpl_UpsertUser_Update(t *testing.T) {

    // the server and request setup from above...
    // ...

    rec := httptest.NewRecorder()
    res := restful.NewResponse(rec)
    wc.ServeHTTP(res, httpReq)

    // ...
    // assertions still to come...

}

This will send our single request without setting up a long-running HTTP server. Once we call this with our mock request and mock response, our response will be populated with everything we need to run our assertions.

Assertions!

This is the easy part. At this point, we're just checking what's in the response, as well as checking what's in the package-private variable. We have access to both, and each can be checked with just a few lines of code:

func TestUserResourceImpl_UpsertUser_Update(t *testing.T) {

    // the server and request setup from above...
    // plus the resposne mocking and wc.ServeHTTP call
    // ...

    if res.StatusCode() != http.StatusOK {
        t.Fatalf("expected status code %d, got %d", http.StatusOK, res.StatusCode())
    }

    if u, _ := users["1"]; u.Age != 40 {
        t.Fatalf("expected Mario's age to be updated to 40, seeing %d", u.Age)
    }
}

And that's it! We can use this same pattern, plus the patterns from the previous post, to fully test this handler function, as well as the rest of the functions. For example, if we want to test the error case, we can use the errReader struct that implements the io.Reader interface:

var testError = "test error"

type errReader struct{}

func (er errReader) Read([]byte) (int, error) {
    return 0, fmt.Errorf(testError)
}

func TestUserResourceImpl_UpsertUser_Error(t *testing.T) {

    // server and endpoint setup
    // ...

    httpReq := httptest.NewRequest(http.MethodPut, "/users/1", errReader{})
    httpReq.Header.Set("Content-Type", restful.MIME_JSON)
    rec := httptest.NewRecorder()
    res := restful.NewResponse(rec)
    wc.ServeHTTP(res, httpReq)

    if res.StatusCode() != http.StatusBadRequest {
        t.Fatalf("expected status code %d, got %d", http.StatusBadRequest, res.StatusCode())
    }

    if rec.Body.String() != testError {
        t.Fatalf("expected error message %q, got %q", testError, rec.Body.String())
    }
}