Go Package for Mocking HTTP Traffic: github.com/h2non/gock
Tags: go, testing
Published: 2021-04-16 16:58:44

Last time I covered testing in Go I mentioned a package used for testing equality called [github.com/google/go-cmp](https://mariocarrion.com/2021/01/22/go-package-equality-google-go-cmp.html), this time I will share with you a way to mock HTTP traffic using the package [`github.com/h2non/gock`](https://github.com/h2non/gock).

## Mocking HTTP traffic

There's a simple CLI tool I built for requesting [OpenWeather](https://openweathermap.org/api) information using their API, please refer to the [final repository](https://github.com/MarioCarrion/videos/tree/main/2021/02/11-go-package-testing-mock-gock) for actually running the full examples.

This CLI allows getting specific details by passing in a zip code as well as the App ID OpenWeather token, outputting something like:

```sh
$ go run main.go -appID <TOKEN> -zip 90210
{Weather:[{Description:broken clouds}] Main:{Temperature:53.24 FeelsLike:48.63}}
```

In practice the important part of this tool that is relevant to this post is the unexported function that does the request called [`requestWeather`](https://github.com/MarioCarrion/videos/blob/87e2bd889ae29f42c15f8fcf79cdfe23e32209d8/2021/02/11-go-package-testing-mock-gock/main.go#L48-L89):

```go
func requestWeather(ctx context.Context, client *http.Client, appID, zip, units string) (Result, error) {
	req, err := http.NewRequestWithContext(ctx,
		http.MethodGet,
		"https://api.openweathermap.org/data/2.5/weather",
		nil)
	if err != nil {
		return Result{}, err // Extra Case, not http-request-based but it is covered in the tests.
	}

	url := req.URL.Query()
	url.Add("zip", zip)
	url.Add("appid", appID)
	url.Add("units", units)

	req.URL.RawQuery = url.Encode()

	res, err := client.Do(req)
	if err != nil {
		return Result{}, err // Case 2: "Third Party Error: when doing request."
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		var errJSON struct {
			Message string `json:"message"`
		}

		if err := json.NewDecoder(res.Body).Decode(&errJSON); err != nil {
			return Result{}, err // Case 4: "Third Party Error: not a 200 status code, can't unmarshal error response."
		}

		return Result{}, fmt.Errorf(errJSON.Message) // Case 3: "Third Party Error: not a 200 status code, returns error in response."
	}

	var result Result

	if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
		return Result{}, err // Case 5: "Third Party Error: 200 status code but is not valid JSON."
	}

	return result, nil // Case 1: "No errors: represented by a 200 status code."
}
```

If we use the number of `return` statements in this function we can determine that at least five subtests are needed to cover all the different paths, and more specifically the need to implement five cases involving HTTP requests:

1. **Case 1**: No errors: represented by a 200 status code.
1. **Case 2**: Third Party Error: when doing request.
1. **Case 3**: Third Party Error: not a 200 status code, returns error in response.
1. **Case 4**: Third Party Error: not a 200 status code, can't unmarshal error response.
1. **Case 5**: Third Party Error: 200 status code but is not valid JSON.

## Structuring tests

The way the tests are implemented in [Test_requestWeather](https://github.com/MarioCarrion/videos/blob/87e2bd889ae29f42c15f8fcf79cdfe23e32209d8/2021/02/11-go-package-testing-mock-gock/main_test.go#L13-L195) is really intentional:

1. [Clean up section](https://github.com/MarioCarrion/videos/blob/87e2bd889ae29f42c15f8fcf79cdfe23e32209d8/2021/02/11-go-package-testing-mock-gock/main_test.go#L14-L17),
1. Disable real networking (`gock.DisableNetworking()`),
1. [Define types and subtests](https://github.com/MarioCarrion/videos/blob/87e2bd889ae29f42c15f8fcf79cdfe23e32209d8/2021/02/11-go-package-testing-mock-gock/main_test.go#L21-L176), and
1. [Run subtests](https://github.com/MarioCarrion/videos/blob/87e2bd889ae29f42c15f8fcf79cdfe23e32209d8/2021/02/11-go-package-testing-mock-gock/main_test.go#L178-L194).

That implementation does not call `t.Paralell()` because `github.com/h2non/gock` is not goroutine safe; similarly the subtest structure is influenced by that limitation where each subtest defines a `setup func()` to explicitly call `h2non/gock` to mock concrete HTTP traffic, for example in [**Case 1**](https://github.com/MarioCarrion/videos/blob/87e2bd889ae29f42c15f8fcf79cdfe23e32209d8/2021/02/11-go-package-testing-mock-gock/main_test.go#L41-L51):

```go
func() {
	gock.New("https://api.openweathermap.org").
		MatchParams(map[string]string{
			"zip":   "90210",
			"appid": "appID",
			"units": "metric",
		}).
		Get("/data/2.5/weather").
		Reply(http.StatusOK).
		File(path.Join("fixtures", "200.json"))
},
```

By doing this we have to opportunity to test all the different paths because in the end we use different arguments for each call preventing previous mocked traffic to conflict with each other.

## h2non/gock API

`h2non/gock`'s API is straightforward, it defines a [`Request`](https://pkg.go.dev/github.com/h2non/gock?utm_source=godoc#Request) type used to match the request to mock, this allows defining concrete configuration values like arguments, body and headers; then a [`Response`](https://pkg.go.dev/github.com/h2non/gock?utm_source=godoc#Response) type is used to return values back to the client, similarly there are methods to indicate concrete headers, status code and the body content, like maps or files.

## Final thoughts

`github.com/h2non/gock` is one of the multiple ways to mock HTTP traffic, its simple yet powerful API allows anyone to quickly learn it and start using it because it covers most of the use cases regarding testing HTTP traffic.

If my project requires integrating with third party HTTP-based APIs, I can always rely on using `github.com/h2non/gock` for mocking purposes. **Highly recommended**.