jarcoal/httpmock

Unable to Mock Responses with Custom http.Client and Transport

battlecook opened this issue · 8 comments

Thank you for creating a good package. I am using a custom http client. If I write the test code as below, it is working good.

func createHttpClient() *http.Client {
	httpClient := &http.Client{}
	return httpClient
}

func doHttp(cli *http.Client, addr string) string {
	reqUrl, _ := url.Parse(addr)
	req := &http.Request{
		Method: http.MethodGet,
		URL:    reqUrl,
	}
	res, err := cli.Do(req)
	if err != nil {
		panic(err)
	}

	body, err := io.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}

	return string(body)
}

func TestHttpMock(t *testing.T) {

	httpmock.Activate()
	defer httpmock.DeactivateAndReset()

	addr := "http://example/foo"
	httpmock.RegisterResponder(http.MethodGet, addr,
		func(req *http.Request) (*http.Response, error) {
			return httpmock.NewStringResponse(http.StatusOK, "bar"), nil
		},
	)

	cli := createHttpClient()

	ret := doHttp(cli, addr)

	assert.Equal(t, "bar", ret)
}

However, I confirmed that I cannot receive a mock response if I add the transport option to createHttpClient as shown below.

func createHttpClient() *http.Client {
	httpClient := &http.Client{
		Transport: &http.Transport{},
	}
	return httpClient
}

Is there a way to make it work even if I add the transport option?

Hello,

httpmock works using its own implementation of http.RoundTripper (i.e. httpmock.MockTransport).

By default, when enabling it globally using httpmock.Activate, it replaces the http.Client.Transport field of http.DefaultClient with httpmock.DefaultTransport.

If you create your own http.Client instance with its own Transport field, then in your tests you must replace this Transport field value by an *httpmock.MockTransport instance produced by NewMockTransport function.

Note that in that case there is no need to call httpmock.Activate function anymore.

func TestHttpMock(t *testing.T) {
	addr := "http://example/foo"

	mockTransport := httpmock.NewMockTransport()
	mockTransport.RegisterResponder(http.MethodGet, addr,
		func(req *http.Request) (*http.Response, error) {
			return httpmock.NewStringResponse(http.StatusOK, "bar"), nil
		},
	)

	cli := createHttpClient()
	cli.Transport = mockTransport

	ret := doHttp(cli, addr)

	assert.Equal(t, "bar", ret)
}

Mmmm, unfortunately this is not viable for libs that assert http.DefaultTransport
See: grafana/grafana-openapi-client-go#94

Asserting that http.DefaultTransport is a specific type is a design mistake. Modifying the pointed data, as it is the case in the code you reference, is even a bigger mistake as it impacts other users of http.DefaultTransport.

Such libraries should just provide a way to override the http.Client.Transport field.

Facing the same issue unfortunately, so I ended up using gock which provides that functionality. https://github.com/h2non/gock?tab=readme-ov-file#mocking-a-custom-httpclient-and-httproundtripper

Hi @emirot,

I didn't understand what gock does that httpmock cannot do, following the link you gave.

If you're talking about gock.InterceptClient function, you can do the same using httpmock:

var client *http.Client// set client
client.Transport = httpmock.DefaultTransport
// or
mock := NewMockTransport()
client.Transport = mock

Note that an equivalent of gock.InterceptClient function can easily be added, as the block code above proves it.

I forgot ActivateNonDefault that already does that :)

@maxatome thanks so much ActivateNonDefault is exactly what I need. I missed it

@emirot feel free to submit a pr to enhance the readme or the godoc 😉