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.
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:
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:
go-github to use our test server instead of
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:
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:
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.
go-github it can look like this:
go.strava it can look like this:
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:
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.
The other problem that I’ve encountered is structs that use
*json.RawMessage to delay the unmarshalling of some fields. They are
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:
Accessing the original payload now requires an additional type assertion:
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
Then we can write a test for a function which only fetches the
github.PushEvent objects like this: