Search code examples
gogo-testing

Let web service response fail on io.ReadAll in unit test


I have a program that calls a web service and I want to write a unit test that checks the correct failure handling (nothing fancy). I use net/http/httptest to simulate the web service (backend). But I haven't figured out so far, how to return a response that fails on the io.ReadAll(res.Body).

The simplified code to call the web service is this:

package main

import (
    "context"
    "io"
    "net/http"
)

func apiCall(url string) ([]byte, error) {
    req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)

    res, _ := http.DefaultClient.Do(req)

    // This is the part I want to check by letting the io.ReadAll fail
    resBody, err := io.ReadAll(res.Body)

    return resBody, err
}

The unit test looks like this:

package main

import (
    "github.com/stretchr/testify/require"
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestIt(t *testing.T) {
    server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
        // How can I make the io.ReadAll(res.Body) return an error?
    }))

    defer server.Close()

    _, err := apiCall(server.URL)
    require.Error(t, err)
}

Maybe I am on the wrong track with the httptest. If there is another or better way to test it, just let me know.

Here is also an link to the playground


Solution

  • I'm not sure it is practical enough, because the error situation I made below doesn't occur normally.

    But if we focus on only the coverage of the code at the test, we can do it like this.

    Code

    func TestIt(t *testing.T) {
        server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
            w.Header().Add("Content-Length", "50")
            _, _ = w.Write([]byte("a"))
        }))
    
        defer server.Close()
    
        _, err := apiCall(server.URL)
        require.Error(t, err)
    }
    

    Then you get an error at io.ReadAll(res.Body) at the apiCall code, saying unexpected EOF

    Go playground. https://go.dev/play/p/GLFiozJb_nO

    Reference. https://github.com/h2non/gock/issues/75

    Why does it happen?

        // ContentLength records the length of the associated content. The
        // value -1 indicates that the length is unknown. Unless Request.Method
        // is "HEAD", values >= 0 indicate that the given number of bytes may
        // be read from Body.
        ContentLength int64
    

    The comment says the above. ContentLength is from the response Content-Length header. Client expects the 50 bytes of data by ContentLength but it encounters EOF right after only a byte. So it returns an error.

    If you want to see the deep-side, you can find it sets the Reader's N (bytes to read) here. You can follow how it is used by using the debug tool at your IDE.