This code is paired with a blog post:
The post describes a method for mocking external client libraries which can be applied to most codebases.
It mocks the client using the context parameter, ensuring all code will receive the same mock for the duration of an operation, and removing the need to explicitly stub a mock in code that might get called during a test.
We use ginkgo for our test suite, but the technique is independent of test framework.
To navigate:
- slackclient/client_test.go is an example test that confirms the mock works
- slackclient/client.go implements the client wrapper and some test helpers
- .circleci/config.yml contains the CI configuration, including things like checking generated code is up-to-date
- .gitattributes configures GitHub to collapse codegen artifacts in PRs by default
An exemplar test might look like this:
var _ = Describe("incident-io/codebase", func() {
var (
sc *mock_slackclient.MockSlackClient
)
slackclient.MockSlackClient(&ctx, &sc, nil)
Describe("some Slack things here", func() {
// Apply an expectation in the BeforeEach, before the test runs
BeforeEach(func() {
sc.EXPECT().GetConversationInfoContext(gomock.Any(), "CH123", false).
Return(&slack.Channel{
GroupConversation: slack.GroupConversation{
Conversation: slack.Conversation{
NameNormalized: "my-channel",
},
},
}, nil).Times(1)
})
Specify("returns a client that responds with the mock", func() {
client, _ := slackclient.ClientFor(ctx, "OR123")
channel, _ := client.GetConversationInfoContext(ctx, "CH123", false)
// We'll only receive this if the client generated by ClientFor is the mock we
// configured with a fake response in our BeforeEach.
Expect(channel.NameNormalized).To(Equal("my-channel"))
})
})
})
This was originally in the blog post, but removed to keep it focused.
For those who are fresh to Go, it's worth explaining that as a language with a deliberate lack of meta-programming features, code generation is the de-facto way to approach several development problems.
This might feel unfamiliar or even awkward coming from other languages, and it certainly makes traversing diffs a bit harder.
When working with generated code, I'd give a few pieces of advice:
-
Add generated codepaths to your
.gitattributes
file aslinguist-generated
. While support is a bit flaky, this will cause GitHub to (mostly) hide generated code when looking at PRs, helping you to focus on the code real humans have written instead of computer generated noise. -
Keep clear boundaries between human and computer generated code. In my example we've generated the client interface into
client_interface.go
, which will contain only computer generated code, but even that may be a bad idea- you'll have the fewest issues if you keep generated code in a separate package, as we've done withmock_slackclient
. -
Add CI steps to check generated code remains up-to-date, both to avoid human changes ending up alongside codegen, but also to catch any accidents where
go generate
was forgotten when checking in the code.
This repo demonstrates all of these, including the CI steps to keep generated code inline with the source.