11

I'm trying to test a method that uses net/http to make requests. Specifically what I'm trying to achieve is to inject a mock http.Client that responds with a certain JSON body

type clientMock struct{}

func (c *clientMock) Do(req *http.Request) (*http.Response, error) {
  json := struct {
    AccessToken string `json:"access_token`
    Scope       string `json:"scope"`
  }{
    AccessToken: "123457678",
    Scope:       "read, write",
  }
  body := json.Marshal(json)
  res := &http.Response {
    StatusCode: http.StatusOK,
    Body:       // I haven't got a clue what to put here
  }
  return res
}

func TestRequest(t *testing.T) { //tests here }

I do know that the Body is of a type io.ReadCloser interface. Trouble is I can't for the life of me find a way to implement it in the mock body response.

Examples as found here so far only demonstrates returning a blank &http.Response{}

3
  • This is probably much easier (and more thorough) to test by mocking the service instead of the client. Take a look at httptest.Server. Commented Nov 22, 2017 at 13:45
  • Make a real request to a mock server. Take a look at how the stdlib does it and use net/http/httptest. Commented Nov 22, 2017 at 13:45
  • You can use ioutil.NopCloser(bytes.NewReader(body)) to set the response Body field. Commented Nov 22, 2017 at 13:49

5 Answers 5

16

While it's probably more useful to mock the full request cycle with httptest.Server, you can use ioutil.NopCloser to create the closer around any reader:

Body: ioutil.NopCloser(bytes.NewReader(body))

and if you want an empty body, just provider a reader with no content.

Body: ioutil.NopCloser(bytes.NewReader(nil))
Sign up to request clarification or add additional context in comments.

4 Comments

Did some diving, httptest.Server particularly ts := httptest.NewServer(http.HandlerFunc(handler)) gives me ats.URL property that I can use to aim my test request at. Problem is my URL is generated on the method I'm testing. I can pass the hostname part of the URL to the method, problem now is my method prepends https:// but httptest.NewServer() doesnt work with https:// it looks like. I also tried httptest.NewTLSServer(). still no luck. any ideas? @JimB
If you need https, then you need httptest.NewTLSServer(). What didn't work?
http: TLS handshake error from 127.0.0.1:49876: remote error: tls: bad certificate
Look at the TLSServer example, you need to use the test client which has the cert loaded in the transport.
6

In your test file (my_test.go):

type MyJSON struct {
        Id          string
        Age         int
}

// Interface which is the same as httpClient because it implements "Do" method.
type ClientMock struct {}

func (c *ClientMock) Do(req *http.Request) (*http.Response, error) {
    mockedRes := MyJSON {"1", 3}

    // Marshal a JSON-encoded version of mockedRes
    b, err := json.Marshal(mockedRes)
    if err != nil {
        log.Panic("Error reading a mockedRes from mocked client", err)
    }

    return &http.Response{Body: ioutil.NopCloser(bytes.NewBuffer(b))}, nil
}

// your test which will use the mocked response
func TestMyFunction(t *testing.T) {

    mock := &ClientMock{}
    actualResult := myFunction(mock)
    assert.NotEmpty(t, actualResult, "myFunction should have at least 1 result")

}

In your implementation (main.go):

package main

import (
    "net/http"
)

func main() {
    myFunction(&http.Client{})
}

Comments

0

I know it's been a little while but I just wrote something to help with this recently.

Like JimB I recommend starting up a real HTTP server locally, since in Go this is easy to do with https://golang.org/pkg/net/http/httptest/.

However having done a lot of HTTP mocking I wanted something that does a little more, like a good mock library would: returning specific data, easy setting of expectations, validation that all requests were made, etc. I have generally used https://godoc.org/github.com/stretchr/testify/mock for mocking and wanted features like that.

So I wrote https://github.com/dankinder/httpmock, which basically combines the two. If you just want a mock that accepts JSON and spits out JSON, it may be an easier way to go.

Comments

0

You can do below:

In your client.go

var cl HTTPClient

type HTTPClient interface {
    Do(req *http.Request) (*http.Response, error)
}

func init() {
    cl = &http.Client{}
}

func Start() error {
    // jsonData: Some JSON marshalled data
    // Url: Some HTTP URL

    req, err := http.NewRequest(http.MethodPost, Url, bytes.NewBuffer(jsonData))
    if err != nil {
        log.Printf("Error in creating new HTTP request: %v", err)
        return err
    }
    req.Header.Set("Content-Type", "application/json")

    resp, err := cl.Do(req)
    if err != nil {
        log.Printf("Error in sending HTTP request: %v", err)
        return err
    }
    defer resp.Body.Close()
    log.Printf("Successfully sent HTTP request")
    return nil
}

In your client_test.go

const errHttpFake = "fake HTTP Error"

type mockDoType func(req *http.Request) (*http.Response, error)

type mockClient struct {
    mockDo mockDoType
}

func (m *mockClient) Do(req *http.Request) (*http.Response, error) {
    return m.mockDo(req)
}

func getHttpFailureClient() {
    req := io.NopCloser(bytes.NewReader([]byte(mockResult)))
    cl = &mockClient{
        mockDo: func(*http.Request) (*http.Response, error) {
            return &http.Response{
                StatusCode: 404,
                Body:       req,
            }, errors.New(errHttpFake)
        },
    }
}

func getHttpSuccessClient() {
    req := io.NopCloser(bytes.NewReader([]byte(mockResult)))
    cl = &mockClient{
        mockDo: func(*http.Request) (*http.Response, error) {
            return &http.Response{
                StatusCode: 200,
                Body:       req,
            }, nil
        },
    }
}

func TestHttpSuccess(t *testing.T) {
    getHttpSuccessClient()
    errHttp := Start() //Replace with function name containing client.Do()
    assert.Nil(t, errHttp)
}

func TestHttpClientFailure(t *testing.T) {
    getHttpFailureClient()
    errHttp := Start() //Replace with function name containing client.Do()
    assert.NotNil(t, errHttp)
}

Comments

0

In your tests you can use the NopCloser:

Body: io.NopCloser(nil),

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.