Basic testing patterns in Go

What should I test?

What you need to test in Go is really not much different from any other programming language. Except maybe that you need to take extra care with pointers if you are not coming from that background. Generally I advocate testing on all levels. Which in practice only means that you need to care for testing not only on the top level layers (e.g. your HTTP endpoints), but also ensure that the underlying structures are also well covered.

Testing of all levels of an application

Should I test unexported functions and methods?
That is somewhat a trick question which I think varies from case to case. In general the unexported functions are called by exported functions in a package. Thus they can be regarded as implementation details of the exported function.

However, in some cases the unexported functions contain significant or important business logic. In which case I think that they should be explicitly tested.

Assertions

When I started testing in Go I wrote my assertions myself. My code could look something like this:

err := someFunc()
if test.err != err {
t.Fatalf("expected err to be %v, but got %v", test.err, err)
}

The assertions could of course get a whole lot more complicated when validating complex structs or such. But even for a basic equality assertion this is unnecessary complex. Why?

  1. Manually writing the validation error message is tedious.
  2. 3 lines of code gets verbose quickly if you have many assertions.

The solution is simple. Start using stretchr/testify! It gives you simple assertions with output that makes sense, like this:

err := someFunc()
assert.Equal(t, test.err, err)

Example output could look like this:

=== RUN   TestAtoi
--- FAIL: TestAtoi (0.00s)
example_test.go:29: Running test case successful conversion
example_test.go:29: Running test case invalid integer
Error Trace: example_test.go:32
Error: Object expected to be of type <nil>, but was *strconv.NumError

Table based testing

My absolute favorite pattern in terms of Go testing is the tabled based tests. The basic structure for a tabled based test is as listed below. The example is testing basic string to integer conversion. There are two test cases in the example; one for a successful integer conversion and one for a non-integer string.

import (
"strconv"
"testing"
    "github.com/stretchr/testify/assert"
)
func TestAtoi(t *testing.T) {
tests := map[string]struct {
input string
output int
err error
}{
"successful conversion": {
input: "1",
output: 1,
err: nil,
},
"invalid integer": {
input: "not an integer",
output: 0,
err: &strconv.NumError{},
},
}
    for testName, test := range tests {
t.Logf("Running test case %s", testName)
        output, err := strconv.Atoi(test.input)
assert.IsType(t, test.err, err)
assert.Equal(t, test.output, output)
}
}

There are a few things to note here:

  1. The test definition is map[string]struct{}, where the test name is the string and the test input data and validation data goes in to the anonymous struct.
  2. We’re looping over each test case and print the test case name using t.Logf(). That makes it easy to debug using go test -v ./....
  3. Naming of validation variables (output and err in this case) should align with the names you use in the test. That makes for logical comparisons like e.g. assert.Equal(t, test.output, output).

One thing I would really like to stress here; Write table based tests, even when you have only one case. It’s not harder to write, you’ll get uniform test files and it’s super easy to extend.

(For vim users I have a snippet to generate tabled based test functions. Check this out!)

Golden file validations

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 ./...

Adios!

That’s all I wanted to cover in this post. Thanks for reading it though! If you have other good practices or objections to any of the approaches above, please do write a comment.

Happy hacking!