64

I'm trying to find a solution to write test and mock HTTP response. In my function where I accept interface:

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

I makes http get request with base auth

func GetOverview(client HttpClient, overview *Overview) (*Overview, error) {

    request, err := http.NewRequest("GET", fmt.Sprintf("%s:%s/api/overview", overview.Config.Url, overview.Config.Port), nil)
    if (err != nil) {
        log.Println(err)
    }
    request.SetBasicAuth(overview.Config.User, overview.Config.Password)
    resp, err := client.Do(request)

How can I mock this HttpClient? I'm looking for mock library, for instance: https://github.com/h2non/gock but there is only mock for Get and Post

Maybe I should do it in a different way. I'll be grateful for advice

quentino
  • 1,101
  • 1
  • 10
  • 25

5 Answers5

65

Any struct with a method matching the signature you have in your interface will implement the interface. For example, you could create a struct ClientMock

type ClientMock struct {
}

with the method

func (c *ClientMock) Do(req *http.Request) (*http.Response, error) {
    return &http.Response{}, nil
}

You could then inject this ClientMock struct into your GetOverview func. Here's an example in the Go Playground.

Gavin
  • 4,365
  • 1
  • 18
  • 27
  • 1
    Oh what I wouldn't give for the `example` / test code to be normal Go unit testing code. e.g. use of the testing package, etc. – Scott Fraley Jul 26 '22 at 20:52
57

The net/http/httptest package is your best friend:

// generate a test server so we can capture and inspect the request
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
    res.WriteHeader(scenario.expectedRespStatus)
    res.Write([]byte("body"))
}))
defer func() { testServer.Close() }()

req, err := http.NewRequest(http.MethodGet, testServer.URL, nil)
assert.NoError(t, err)

res, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
assert.Equal(t, scenario.expectedRespStatus, res.StatusCode, "status code should match the expected response")
tu4n
  • 4,200
  • 6
  • 36
  • 49
  • 2
    I don't see how this could be used to confirm that a function calls `http.DefaultClient.Do` with a specific external URL. Is that possible? – ldanilek Sep 19 '18 at 18:08
  • No, you can't. In that case, you need to create a wrapping INTERFACE and do mocking on top. I often use this https://github.com/vektra/mockery – tu4n Oct 03 '18 at 11:30
  • @rocketspacer: I am having issues with `httpmock` causing "data race" conditions when used to run tests via ginkgo which parallelizes tests and also when the code includes multiple goroutines. Is using `http.DefaultClient` thread safe? Thank you! – aqn Apr 25 '19 at 13:32
  • According to Go documentation `http.Client` is concurrently-safe https://golang.org/pkg/net/http/#Client – tu4n Dec 11 '19 at 08:39
9

You have to create a struct with methods that match interface. Mocks are commonly used for testing purposes, therefore people want the ability to prepare return values of mock methods. To achieve this, we create struct with func attributes corresponding to methods.

As your interface is:

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

Equivalent mock:

type MockClient struct {
    DoFunc func(req *http.Request) (*http.Response, error)
}

func (m *MockClient) Do(req *http.Request) (*http.Response, error) {
    if m.DoFunc != nil {
        return m.DoFunc(req)
    }
    return &http.Response{}, nil
}

Then, next step is to write some tests. Example here.

mwarzynski
  • 294
  • 1
  • 7
  • How can I verify multiple scenarios with this approach? Let's say I want to test multiple return codes and errors. – rajk Feb 04 '19 at 08:13
  • @rajk You may overwrite the `DoFunc` method to behave differently. Notice that you shouldn't run tests in parallel with this approach. – mwarzynski May 07 '19 at 22:18
3

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

Generally to mock HTTP requests I recommend starting up a real HTTP server locally, since in Go this is easy to do. https://golang.org/pkg/net/http/httptest/ is a pretty standard way to do that (see the example code given under the Server type).

However having done a lot of HTTP mocking I wanted something that does a little more, like a good mock library would: 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.

Dan
  • 73
  • 5
  • 7
    While adding links to references is a good idea, it is important that there is enough context in the answer so it can stand on its own if the links go dead in the future. Adding a code example or two would surely help with that and prevent your answer from being deleted during review. – NightOwl888 Apr 26 '18 at 17:21
-1

If you are using the Client from http library, I would prefer using a RoundTripper to do this.

You should define a RoundTripper first

type mockRoundTripper struct {
    response *http.Response
}

func (rt *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
    return rt.response, nil
}

You unittest test would look like this:

func TestGetOverview(t *testing.T) {
    // Set up your response
    json := `{"code": 0, "message": "success"}`
    recorder := httptest.NewRecorder()
    recorder.Header().Add("Content-Type", "application/json")
    recorder.WriteString(json)
    expectedResponse := recorder.Result()

    // Create an HTTP client
    client := http.Client{Transport: &mockRoundTripper{expectedResponse}}

    // Call your function
    overview, err := yourlib.GetOverview(client, &yourlib.Overview{})
    ....
}

Here is a simple example: https://go.dev/play/p/wtamTRahsZX

Xavier-Lam
  • 404
  • 4
  • 6