Mocking an HTTP call with a fixed URL


At work I ran into the need to mock an HTTP call to a fixed address.

The code being tested verifies that the URL being called is legitimate and follows a certain set of rules. Therefore it was impossible to use the address of a local server (at least not directly).

To write tests for my code, I needed to write something that mocked the return values of a valid endpoint without actually making a call to the original, validated address.

I found two approaches to solve this problem.

Approach 1

The first approach to this problem was to create a custom Dialer using struct embedding:

type fakeDialer struct {
	*net.Dialer
	server string
}

func (d *fakeDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
	return d.Dialer.DialContext(ctx, network, strings.TrimLeft(d.server, "http://"))
}

We start a local testing server (like httptest):

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "This is a test")
	}))
	defer ts.Close()
}

And we create a dialer and attach it to a client:

fDialer := &fakeDialer{Dialer: &net.Dialer{
	Timeout:   30 * time.Second,
	KeepAlive: 30 * time.Second,
}, server: ts.URL}

fakeTransport := &http.Transport{
	DialContext: fDialer.DialContext,
}

client := &http.Client{Transport: fakeTransport}
fDialer := &fakeDialer{Dialer: &net.Dialer{
	Timeout:   30 * time.Second,
	KeepAlive: 30 * time.Second,
}, server: ts.URL}

This results in every call made with the client we created being hijacked so that a call is sent to our test server instead of the outside world.

The benefits of this approach is that we’re still going through the entire process of making an HTTP call which allows us to find any defects along the way in this code path.

A working example can be found here: https://go.dev/play/p/UI7pEcZ_5lE

Approach 2

The second approach is to hook into a different place in the HTTP client code by creating a custom type that implements the RoundTripper interface.

We start by implementing the RoundTripper interface:

type fakeTransport struct {
	body     string
	httpCode int
}

func (t *fakeTransport) RoundTrip(_ *http.Request) (*http.Response, error) {
	return &http.Response{
		Body:       io.NopCloser(strings.NewReader(t.body)),
		StatusCode: t.httpCode,
	}, nil
}

And then, just like we did in the first approach, we attach it to a client:

client := &http.Client{Transport: &fakeTransport{body: "This is a test\n", httpCode: 200}}

Now, when we make HTTP calls, we don’t actually do anything network related, we simply get back an http.Response struct from the function we created to satisfy the RoundTripper interface.

Conclusion

These are two approaches to mocking HTTP calls when using a fixed URL. The difference between the two approaches is subtle: one exercises the networking stack by actually making an HTTP call (Approach 1) while the other bypasses the network stack and returns a struct controlled by us as a response (Approach 2). Both are useful approaches as long as the implications of each are understood.

In the end, we decided to go with Approach 2. It achieves the goal of covering the code paths I set out to cover and it is easier to understand. However, this means the testing will be missing some coverage since the networking stack is not invovled.

In follow up work, an end to end test will be used to cover potential failures that can arise when complications from the networking stack spring up and gain further confidence that our code works as intended.

This post was made better with valuable feedback from