DEV Community

Mario Carrion
Mario Carrion

Posted on • Originally published at mariocarrion.com on

Go Package for Mocking HTTP Traffic: github.com/h2non/gock

Last time I covered testing in Go I mentioned a package used for testing equality called github.com/google/go-cmp, this time I will share with you a way to mock HTTP traffic using the package github.com/h2non/gock.

Mocking HTTP traffic

There's a simple CLI tool I built for requesting OpenWeather information using their API, please refer to the final repository 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:

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

In practice the important part of this tool that is relevant to this post is the unexported function that does the request called requestWeather:

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."
}
Enter fullscreen mode Exit fullscreen mode

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.
  2. Case 2: Third Party Error: when doing request.
  3. Case 3: Third Party Error: not a 200 status code, returns error in response.
  4. Case 4: Third Party Error: not a 200 status code, can't unmarshal error response.
  5. Case 5: Third Party Error: 200 status code but is not valid JSON.

Structuring tests

The way the tests are implemented in Test_requestWeather is really intentional:

  1. Clean up section,
  2. Disable real networking (gock.DisableNetworking()),
  3. Define types and subtests, and
  4. Run subtests.

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:

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"))
},
Enter fullscreen mode Exit fullscreen mode

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 type used to match the request to mock, this allows defining concrete configuration values like arguments, body and headers; then a 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.

Top comments (0)