Search code examples
unit-testinggomockinghttpserver

Mock ioUtils.ReadAll to fail when reading body of httpresponse, without using httptest


I am currently trying to avoid httpserver. Reason behind is that I am trying a different approach and write all my unit tests by mocking the httpClient, passed as interface to my logic.

The problem I am having at the moment is that I want my logic, see right below, to fail when the response cannot be read:

defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
    return nil, fmt.Errorf("Error reading response body: %w", err)
}

where res comes from:

res, err := myClient.Do(req)
if err != nil {
    return nil, fmt.Errorf("Could not submit file: %w", err)
}

myClient is of an interface type that implements the Do method, hence I am able to mock it. After reading some questions on the related matter I tried to mock my client's Do method to return:

response := http.Response{
    Body:          ioutil.NopCloser(bytes.NewBufferString("")),
    ContentLength: 1
}

I based myself on top of this question. Unfortunately this doesn't work and my code is still able to read the body without generating an error.


Solution

  • I've built a sample program to help you understand how to deal with this scenario. Let's see the two files involved:

    main.go

    package mockhttpbody
    
    import "net/http"
    
    func DummyServer(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Length", "1")
    }
    

    This file is the same one you take from the other question, so I won't cover anything here.

    main_test.go

    package mockhttpbody
    
    import (
        "fmt"
        "io"
        "net/http"
        "net/http/httptest"
        "testing"
    )
    
    type mockReader struct{}
    
    func (m *mockReader) Read(p []byte) (n int, err error) {
        return 0, io.ErrUnexpectedEOF
    }
    
    func Test(t *testing.T) {
        req := httptest.NewRequest(http.MethodGet, "/example", nil)
        w := httptest.NewRecorder()
    
        DummyServer(w, req)
    
        data, err := io.ReadAll(&mockReader{})
        if err != nil {
            panic(err)
        }
    
        fmt.Println(string(data))
    }
    

    Let's tackle this step-by-step.

    io package

    I suggest you use the package io instead of ioutil as the latter has been deprecated.

    Reader implementation

    The mock that you really need for what you want to achieve is of this interface:

    type Reader interface {
        Read(p []byte) (n int, err error)
    }
    

    This comes into play when you issue the io.ReadAll function. Here you've to pass your own custom implementation. You can define your implementation with the following code:

    type mockReader struct{}
    
    func (m *mockReader) Read(p []byte) (n int, err error) {
        return 0, io.ErrUnexpectedEOF
    }
    

    Then, in your test code, you have to use this data, err := io.ReadAll(&mockReader{}) (so the result returned from the mock HTTP server is useless).

    ErrUnexpectedEOF

    Last, the io.ReadAll function doesn't treat the EOF as an error so you should use something different. The error I choose is the ErrUnexpectedEOF one but it's up to you this decision.

    Let me know if this clarifies a little bit!