Skip to Content

Mocking APIs in Go Tests

There are two patterns that I’ve come to really appreciate when testing Go code that uses libraries to access third-party APIs. They aren’t necessarily specific to Go. I came to Go from Ruby and Python, so this might actually be an “ode to static typing” in disguise.

Examples

I’m going to use the Ginkgo / Gomega testing framework in my code examples, though the same functionality can be achieved using the standard library and some helper code. I’ll also reference two libraries that I’ve been using recently:

Patterns

Injecting mock servers

In Ruby it’s possible and commonplace (though not necessarily desirable) to monkey-patch objects at runtime, which can be used in tests to change the behaviour of libraries and their underlying dependencies. It can be very powerful but equally very difficult understand what’s going on.

Go doesn’t support monkey-patching. In most cases you need to structure your code in a way that you can use dependency injection (in its simplest form; no magic frameworks) to pass alternate objects for testing. Sometimes you may need to use custom interfaces as test doubles, but other times the configuration you need will already be exposed, either for the library’s own tests or other functionality.

We start by creating a new HTTP server where we can make assertions on requests received and generate our own responses. We need the URL to give to our client:

var server *ghttp.Server

BeforeEach(func() {
  server = ghttp.NewServer()

  serverURL, err := url.Parse(server.URL())
  Expect(err).ToNot(HaveOccurred())
})

Telling go-github to use our test server instead of github.com requires very little effort because github.Client exposes a public field called BaseURL, which is foremost intended for using the library against GitHub Enterprise, but is also used by the library’s own tests.

We can do the same to point a client at our test server:

var client *github.Client

BeforeEach(func() {
  
  client = github.NewClient(nil)
  client.BaseURL = serverURL
})

Telling go.strava to use our test server requires a few more lines of code. Like a lot of HTTP libraries (including go-github) it allows you to pass your own http.Client to the client constructor. This is powerful because it allows you to provide an HTTP client that implements authentication, or caching, or any other kind of request/response manipulation. The library’s own tests do this to provide an http.Transport that returns responses from fixture strings and files.

We can do something similar to make sure that all requests connect to our test server:

var client *strava.Client

BeforeEach(func() {
  
  dialMock := func(network, addr string) (net.Conn, error) {
    return net.Dial(network, serverURL.Host)
  }

  httpClient := &http.Client{
    Transport: &http.Transport{
      Dial:    dialMock,
      DialTLS: dialMock,
    },
  }

  client = strava.NewClient("token", httpClient)
})

Mocking JSON responses

Now that we’re getting requests, we need to generate the right responses. For the APIs that we’re dealing with the response bodies are JSON document strings.

Generating these is made easier by Go’s encoding/json package which converts JSON to structs and vice versa. We can use the public structs from the library instead of handwriting the JSON ourselves, which is more succinct and benefits from type checking, so you’ll get fast feedback if you misspell a field name or the structure of the API and library change. For some tests we can also get away with only populating a subset of fields.

For go-github it can look like this:

const org = "acme"
var fixture []github.User

BeforeEach(func() {
  fixture = []github.User{
    {ID: github.Int(1), Login: github.String("one")},
    {ID: github.Int(2), Login: github.String("two")},
    {ID: github.Int(3), Login: github.String("three")},
  }

  server.AppendHandlers(
    ghttp.CombineHandlers(
      ghttp.VerifyRequest("GET", fmt.Sprintf("/orgs/%s/members", org)),
      ghttp.RespondWithJSONEncoded(http.StatusOK, fixture),
    ),
  )
})

It("should return fixture of users", func() {
  opts := &github.ListMembersOptions{}
  result, _, err := client.Organizations.ListMembers(org, opts)
  Expect(err).ToNot(HaveOccurred())
  Expect(result).To(Equal(fixture))
})

For go.strava it can look like this:

var fixture []*strava.ActivitySummary

BeforeEach(func() {
  now := time.Now()
  athlete := strava.AthleteSummary{CreatedAt: now, UpdatedAt: now}

  fixture = []*strava.ActivitySummary{
    {Id: 1, Name: "one", Athlete: athlete, StartDate: now, StartDateLocal: now},
    {Id: 2, Name: "two", Athlete: athlete, StartDate: now, StartDateLocal: now},
    {Id: 3, Name: "three", Athlete: athlete, StartDate: now, StartDateLocal: now},
  }

  server.AppendHandlers(
    ghttp.CombineHandlers(
      ghttp.VerifyRequest("GET", "/api/v3/athlete/activities"),
      ghttp.RespondWithJSONEncoded(http.StatusOK, fixture),
    ),
  )
})

It("should return fixture of activities", func() {
  result, err := strava.NewCurrentAthleteService(client).ListActivities().Do()
  Expect(err).ToNot(HaveOccurred())
  Expect(result).To(Equal(fixture))
})

The above examples are simplistic because they’re testing behaviour that should be covered by the library’s own tests. This comes in more useful when testing your own code that wraps the library to perform error handling or pagination. For example:

func PaginationHeader(url string, next int) http.Header {
  headers := http.Header{}
  headers.Set("Link", fmt.Sprintf(`<%s?page=%d>; rel="next"`, url, next))

  return headers
}

BeforeEach(func() {
  server.AppendHandlers(
    ghttp.CombineHandlers(
      ghttp.VerifyRequest("GET", path),
      ghttp.RespondWithJSONEncoded(http.StatusOK, fixture[0:2],
        PaginationHeader(server.URL()+path, 1)),
    ),
    ghttp.CombineHandlers(
      ghttp.VerifyRequest("GET", path),
      ghttp.RespondWithJSONEncoded(http.StatusOK, fixture[2:],
        PaginationHeader(server.URL()+path, 0)),
    ),
  )
})

It("should return fixture of users", func() {
  result, err := GetMembersAllPages(client, org)
  Expect(err).ToNot(HaveOccurred())
  Expect(result).To(Equal(fixture))
})

Problems

time.Time

The last go.strava example contains quite a lot of additional fields in the fixture. These are required in order to compare the fixture and result, because marshalling and unmarshalling a zero time.Time object does not produce something that has == equality. An alternative to populating the fields manually could be to use an intermediate function or custom matcher to convert or ignore those fields.

json.RawMessage

The other problem that I’ve encountered is structs that use *json.RawMessage to delay the unmarshalling of some fields. They are used by github.Event because the payload is one of many types, such as github.PushEvent. This is troublesome for writing tests that use slices and refer to both the inner and outer objects because it requires more than one operation to construct or reference each one.

Constructing one of these objects now requires several operations:

event := &github.Event{
  ID:   github.String("201"),
  Type: github.String("PushEvent"),
}
payload := &github.PushEvent{
  PushID: github.Int(101),
  Ref:    github.String("aaaaaaa"),
}

payloadJSON, err := json.Marshal(payload)
Expect(err).ToNot(HaveOccurred())

payloadRaw := json.RawMessage(payloadJSON)
event.RawPayload = &payloadRaw

Accessing the original payload now requires an additional type assertion:

pushEvent, ok := event.Payload().(*github.PushEvent)
Expect(ok).To(BeTrue())
Expect(pushEvent).To(Equal(payload))

We can cheat by defining a new struct that embeds the two types that we need in a way that when marshalled it produces the equivalent github.Event JSON:

type EventFixture struct {
  *github.Event
  RawPayload *github.PushEvent `json:"payload,omitempty"`
}

Then we can write a test for a function which only fetches the github.PushEvent objects like this:

var pushEventFixture []*github.PushEvent

BeforeEach(func() {
  pushEventFixture = []*github.PushEvent{
    {
      PushID: github.Int(101),
      Ref: github.String("aaaaaaa"),
    }, {
      PushID: github.Int(102),
      Ref: github.String("bbbbbbb"),
    },
  }

  eventFixture := []*EventFixture{
    {
      Event: &github.Event{ID: github.String("201"), Type: github.String("PushEvent")},
      RawPayload: pushEventFixture[0],
    }, {
      Event: &github.Event{ID: github.String("202"), Type: github.String("PushEvent")},
      RawPayload: pushEventFixture[1],
    },
  }

  server.AppendHandlers(
    ghttp.CombineHandlers(
      ghttp.VerifyRequest("GET", path),
      ghttp.RespondWithJSONEncoded(http.StatusOK, eventFixture),
    ),
  )
})

It("should return fixture of pushevents", func() {
  result, err := GetPushEvents(client, user)
  Expect(err).ToNot(HaveOccurred())
  Expect(result).To(Equal(pushEventFixture))
})