How to mock http.Client that returns a JSON response
Asked Answered
A

5

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{}

Arieariel answered 22/11, 2017 at 13:43 Comment(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.Arin
Make a real request to a mock server. Take a look at how the stdlib does it and use net/http/httptest.Andry
You can use ioutil.NopCloser(bytes.NewReader(body)) to set the response Body field.Tyrr
H
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))
Harbour answered 22/11, 2017 at 13:49 Comment(4)
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? @HarbourArieariel
If you need https, then you need httptest.NewTLSServer(). What didn't work?Harbour
http: TLS handshake error from 127.0.0.1:49876: remote error: tls: bad certificateArieariel
Look at the TLSServer example, you need to use the test client which has the cert loaded in the transport.Harbour
I
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{})
}
Introgression answered 30/10, 2018 at 13:35 Comment(0)
K
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.

Kymberlykymograph answered 26/4, 2018 at 17:5 Comment(0)
N
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)
}
Nielson answered 7/6, 2023 at 9:6 Comment(0)
H
0

In your tests you can use the NopCloser:

Body: io.NopCloser(nil),
Houseline answered 12/8, 2024 at 10:11 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.