Go Best Practices — Testing

I the early days of my programming career I didn’t really see the value and mainly thought it duplicated work. Now however I usually aim for 90-100% test coverage on everything I write. And I generally believe that testing on every layer is a good practice (we’ll get back to this).

In fact, when looking at the code bases I have in front of me daily, the ones I fear the most to change are the ones with the least test coverage. And that ultimately decreases my productivity and our deliverables. So to me it’s pretty clear that high test coverage both equals higher quality and higher productivity.

Testing on every layer

We’ll dive into an example right away. Assume you have an app with the following structure.

Application model

There are some shared components, like the models and the handlers. Then you have a couple of different ways of interacting with this application, e.g. CLI, HTTP API or Thrift RPCs. I have found it a good practice to make sure you test not only the models or only the handlers, but all of them. Even for the same feature. Because it’s but necessarily true that if you’ve implemented support for Feature X in the handler, that it’s actually available via the HTTP and Thrift interfaces for example.

This was you will be more confident making changes to your logic, even deep down in the core of the application.

Table based tests

In almost all cases when you test a method you’d like to test a couple of scenarios on the function. Usually with different input parameters or different mock responses. I like to group all these tests into one Test* function and then have a loop running through all the test cases. Here’s a basic example:

func TestDivision(t *testing.T) {
tests := []struct{
x float64
y float64
result float64
err error
}{
{ x: 1.0, y: 2.0, result: 0.5, err: nil },
{ x: -1.0, y: 2.0, result: -0.5, err: nil},
{ x: 1.0, y: 0.0, result: 0.0, err: ErrZeroDivision},
}
    for _, test := range tests {
result, err := divide(test.x, test.y)
assert.IsType(t, test.err, err)
assert.Equal(t, test.result, result)
}
}

The above tests are not covering everything, but the serve as an example for how to test for expected results and errors. The above code also uses the great testify package for assertions.

An enhancement, table based tests with named test cases

If you have many tests or often new developers that are not familiar with the code base it can be useful to name your tests. Here’s a short example of what that would look like

tests := map[string]struct {
number int
smsErr error
err error
}{
"successful": {0132423444, nil, nil},
"propagates error": {0132423444, sampleErr, sampleErr},
}

Note that there is a difference here between having a map and a slice. The map does not guarantee order, while the slice does.

Mocking using mockery

Interfaces are naturally super good integration points for tests, since the implementation of an interface can easily be replaced by a mock implementation. However, writing mocks can be quite tedious and boring. To make life easier I’m using mockery to generate my mocks based on a given interface.

Let’s take a look on how to work with that. Assume we have the following interface.

type SMS interface {
Send(number int, text string) error
}

Here’s a dummy implementation using this interface:

// Messager is a struct handling messaging of various types.
type Messager struct {
sms SMS
}
// SendHelloWorld sends a Hello world SMS.
func (m *Messager) SendHelloWorld(number int) error {
err := m.sms.Send(number, "Hello, world!")
if err != nil {
return err
}
    return nil
}

We can now use Mockery to generate a mock for the SMS interface. Here’s what that would look like (this example is using the -inpkg flag which puts the mock in the same package as the interface).

// MockSMS is an autogenerated mock type for the SMS type
type MockSMS struct {
mock.Mock
}
// Send provides a mock function with given fields: number, text
func (_m *MockSMS) Send(number int, text string) error {
ret := _m.Called(number, text)
    var r0 error
if rf, ok := ret.Get(0).(func(int, string) error); ok {
r0 = rf(number, text)
} else {
r0 = ret.Error(0)
}
    return r0
}
var _ SMS = (*MockSMS)(nil)

The SMS struct is inheriting from testify mock.Mock, which gives us some interesting options when writing the test cases. So, now it’s time to write our test for the SendHelloWorld method using the mock from Mockery.

func TestSendHelloWorld(t *testing.T) {
sampleErr := errors.New("some error")
    tests := map[string]struct {
number int
smsErr error
err error
}{
"successful": {0132423444, nil, nil},
"propagates error": {0132423444, sampleErr, sampleErr},
}
    for _, test := range tests {
sms := &MockSMS{}
sms.On("Send", test.number, "Hello, world!").Return(test.smsErr).Once()
        m := &Messager{
sms: sms,
}

err := m.SendHelloWorld(test.number)
assert.Equal(t, test.err, err)
        sms.AssertExpectations(t)
}
}

There are a couple of points worth mentioning in the above example code. In the test you’ll notice that I instantiate MockSMS and then using .On() I can dictate what should happen (.Return()) when certain parameters are sent to the mock.

Finally I’m using sms.AssertExpectations to make sure that the SMS interface has been called the expected number of times. In this case Once() .

All files above can be found in this gist.

Golden file tests

In some cases I’ve found it useful to be able to just assert that a big response blob remains the same. Could for example be data returned from a JSON data from an API. For that case I learnt from Michell Hashimoto about the use of golden files combined with a smart was of exposing command line flags to go test.

The basic idea is that you’d write the correct response body to a file (the golden file). Then when running the tests you do a byte comparison between the golden file and the test response.

To make it easier I’ve created the goldie package, which handles the command line flag setting and golden file writing and comparison transparently.

Here’s an example of how to use goldie for this type of testing:

func TestExample(t *testing.T) {
recorder := httptest.NewRecorder()

req, err := http.NewRequest("GET", "/example", nil)
assert.Nil(t, err)

handler := http.HandlerFunc(ExampleHandler)
handler.ServeHTTP()

goldie.Assert(t, "example", recorder.Body.Bytes())
}

When you need to update your golden file you’d run the following:

go test -update ./...

And when you just want to run the tests, you’d do that as usual:

go test ./...

Bye bye!

Thanks for sticking around to the end! Hope you’ve found something useful in the article.