Guidance Testing HTTP Server


(Matt) #1

Hi, I’m fairly new to Go and looking for some guidance testing a HTTP server implementation.

The behaviors I’m trying to test

  • Request received on /mypath
    • Method must be a POST; all other methods return 405 error
      • Request must have a valid JSON body
        • missing body send 422
        • missing required fields send 422

These behaviors are written by me. Maybe I’m misguided by my understanding of RESTful best practice. I’m trying to avoid the frustrations I’ve experienced implementing a API client due to inaccurate status codes. I want users of my API to be able to understand what is wrong with their client quickly and I believe accurate status codes will assist with this.

My tests looks like:

func TestServer(t *testing.T) {
	server := NewServer()

	t.Run("Send GET, expect 405", func(t *testing.T) {
		request, _ := http.NewRequest(http.MethodGet, "/myPath", nil)
		response := httptest.NewRecorder()

		server.ServeHTTP(response, request)
        assertResponseStatus(t, got, http.StatusMethodNotAllowed)
	})

	t.Run("Send POST without body, expect 422", func(t *testing.T) {
		var body = []byte(``)
		request, _ := http.NewRequest(http.MethodPost, "/myPath", nil)
		response := httptest.NewRecorder()

		request.Header.Set("Content-Type", JSONContentType)

		server.ServeHTTP(response, request)

        assertResponseStatus(t, got, http.StatusUnprocessableEntity)
	})
}

I realize there is some duplication here, I haven’t refactored yet. I also haven’t written tests for “all other methods return 405”, just GET so far. And I won’t touch on “missing required fields send 422” yet.

I’m using gorilla/mux.

“Method must be a POST all other methods return 405” is satisfied by

router.Handle("/mypath", http.HandlerFunc(myPathHandler)).Methods("POST")

“Send POST without body, expect 422” is where I’m having issues.

How do I build a request with an empty body? I’ve tried

request, _ := http.NewRequest(http.MethodPost, "/mypath", nil)

but this results in a Null Pointer when trying

func myPathHandler(w http.ResponseWriter, r *http.Request) {
  myPathOpts := &myPathRequestBody{}

  err := json.NewDecoder(r.Body).Decode(myPathOpts)
  ...
}

So I try

func myPathHandler(w http.ResponseWriter, r *http.Request) {
    body, err := ioutil.ReadAll(r.Body)
	switch {
	case err == io.EOF:
		http.Error(w, err.Error(), http.StatusUnprocessableEntity)
	case err != nil:
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
    ...
}

but this is also a Null Pointer

I build the request differently

request, _ := http.NewRequest(http.MethodPost, "/myPath", bytes.NewBuffer(nil))

or

var body = []byte(``)
request, _ := http.NewRequest(http.MethodPost, "/myPath", bytes.NewBuffer(body))

but I don’t get an io.EOF or an err != nil

Add a switch case to test body == nil

func myPathHandler(w http.ResponseWriter, r *http.Request) {
	body, err := ioutil.ReadAll(r.Body)
	switch {
	case body == nil:
		http.Error(w, err.Error(), http.StatusUnprocessableEntity)
	case err == io.EOF:
		http.Error(w, err.Error(), http.StatusUnprocessableEntity)
	case err != nil:
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
    ...
}

no luck there either.

Goodness, my ? seems a bit verbose. Thank you for your time.

Guidance and criticism greatly appreciated.

Kindly, Matt


(Johan Dahl) #2

Hi

Im not sure but a 422 is just that you expected a certain json structure but for example got the empty string or the wrong json structure. You maybe expected

{"user": "Alex", "email": "alex@gmail.com"}

but got a empty string or

[1, 3, 44, "cat"]

instead. It is valid json but not with the structure you expected. A post will always have a body even if it is just zero bytes long.

https://tools.ietf.org/html/rfc4918#section-11.2

The 422 (Unprocessable Entity) status code means the server
understands the content type of the request entity (hence a
415(Unsupported Media Type) status code is inappropriate), and the
syntax of the request entity is correct (thus a 400 (Bad Request)
status code is inappropriate) but was unable to process the contained
instructions. For example, this error condition may occur if an XML
request body contains well-formed (i.e., syntactically correct), but
semantically erroneous, XML instructions.

So it would be


(Matt) #3

go version go1.11.2 linux/amd64

@johandalabacka, thank you for taking the time to respond.

I’m not understanding why err == io.EOF doesn’t match. I’m reading the comment from source
and obviously missing something. I’m just curious as to where my misunderstanding is.

https://golang.org/pkg/net/http/#Request

// Body is the request’s body.
//
// For client requests a nil body means the request has no
// body, such as a GET request. The HTTP Client’s Transport
// is responsible for calling the Close method.
//
// For server requests the Request Body is always non-nil
// but will return EOF immediately when no body is present.
// The Server will close the request body. The ServeHTTP
// Handler does not need to.
Body io.ReadCloser

func myPathHandler(w http.ResponseWriter, r *http.Request) {
    body, err := ioutil.ReadAll(r.Body)
	switch {
	case err == io.EOF:
		http.Error(w, err.Error(), http.StatusUnprocessableEntity)
	case err != nil:
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
    ...
}

I was able to get what I wanted by checking len(body) == 0

See https://gist.github.com/mahummon/90207f32c3247eca57b002ddf00f510b

You’ll see in the gist that there is only one test. The test with an explicit empty body. I can’t get the test with a nil body to run without Null Pointer exception.

t.Run("Send POST with nil body, expect 422", func(t *testing.T) {
  request := newRequest(t, http.MethodPost, "/", nil)
  response := httptest.NewRecorder()

  server.ServeHTTP(response, request)

  got := response.Code

  assertResponseStatus(t, got, http.StatusUnprocessableEntity)

})

I guess this is just a bad test.