Intermediate Testing in Golang
I've been writing Golang in production at Monzo for a couple of months now. This post shares some things I've learnt about testing Go code.
Parallelisation
Golang tests can be easily parallelised by calling t.Parallel()
at the
beginning of a test:
func TestThing(t *testing.T) {
t.Parallel()
// test code
}
This test will now be run in parallel with other tests marked as parallel.
Parallel tests are run in a number of goroutines. This number is defaulted to
GOMAXPROCS
, but can be set by passing the -parallel
flag to go test
.
Skipping tests
Sometimes it's useful to skip a particular test. If a change causes multiple
tests to fail, I like to skip all but one, to reduce noise while debugging. I
used to do this by commenting out functions, but it can also be done by calling
t.SkipNow()
at the beginning of the test:
func TestThing(t *testing.T) {
t.SkipNow()
// test is skipped
}
SkipNow
should be called at the top of the function. If a test fails before
SkipNow
is called, the test is still considered to have failed.
SkipNow
is useful if skipping a test needs to be committed to version control.
Explicitly skipping a test is less confusing to future readers than a commented
test. Was the commented out test done so intentionally? Is that test meant to
pass? Skipping can be made even more explicit by calling Skip
or Skipf
,
which logs a (formatted) message before skipping the test.
func TestThing(t *testing.T) {
t.Skip("Temporarily skipping test because ...")
}
Sub tests
A popular paradigm in Golang is table-driven testing, where a single test is run with an array of different input parameters:
func TestAdd(t *testing.T) {
testCases := []struct{
a, b, expected int
}{
{a: 0, b: 0, expected: 0},
{a: -1, b: 1, expected: 0}
}
for _, tc := range testCases {
if tc.a + tc.b != tc.expected {
t.Errorf("%d + %d != %d", tc.a, tc.b, tc.expected)
}
}
}
Although this is written as a single test, it's really multiple. If one of the test cases fails, the whole test function fails, and it can be difficult to tell which one failed without a useful error message.
The testing
package contains a function which allows tests like this to be
split out into explicit sub-tests:
func TestAdd(t *testing.T) {
testCases := []struct{
a, b, expected int
}{
{a: 0, b: 0, expected: 0},
{a: -1, b: 1, expected: 0}
}
for _, tc := range testCases {
name := fmt.Sprintf("%d + %d != %d", tc.a, tc.b, tc.expected)
t.Run(name, func(t *testing.T) {
t.Parallel()
if tc.a + tc.b != tc.expected {
t.Error("incorrect addition")
}
})
}
}
Each of these subtests can also now be parallelised.
Assertions
Golang doesn't have a built in assert
function. Manual comparison is the
recommended way to check that two objects are the same. This can be verbose, and
the comparison method depends of the object's type:
func TestThing(t *testing.T) {
a := 0
b := 1
if a != b {
t.Errorf("%d != %d", a, b)
}
c := []int{1, 2, 3}
d := []int{4, 5, 6}
// Comparing slices is different to comparing ints
if !reflect.DeepEqual(c, d) {
t.Errorf("%v != %v", c, d)
}
}
I've started using the assert
package to simplify this
code:
func TestThing(t *testing.T) {
a := 0
b := 1
assert.Equal(t, a, b)
c := []int{1, 2, 3}
d := []int{4, 5, 6}
// ✨ API is now the same
assert.Equal(t, c, d)
}
assert
comes with lots of functions for verifying your data.
Examples
Golang offers an easy way to write example functions which are displayed on the package's godoc page. These example functions are defined alongside a package's tests, and are run with the tests, to make sure they aren't outdated.
If there's any ambiguity about how a function should be used, I like to add an example to make it easier to understand.
Helper functions
Sometimes it's useful to refactor code used in multiple tests into a helper function:
package main
import "testing"
func TestThing(t *testing.T) {
AssertEqualInt(t, 1, 2)
}
func AssertEqualInt(t *testing.T, a, b int) {
if a != b {
t.Errorf("%d != %d", a, b)
}
}
When we run this test, we get the error:
$ go test .
--- FAIL: TestThing (0.00s)
scratch_test.go:11: 1 != 2
This error message tells us which test failed, but the line given (11), is the
line that Errorf
is called. If this helper is called multiple times, it can be
difficult to work out which call is erroring.
Golang provides a way, t.Helper
, to explicitly mark that function as a helper:
func AssertEqualInt(t *testing.T, a, b int) {
t.Helper()
if a != b {
t.Errorf("%d != %d", a, b)
}
}
go test
now prints out the line number where the helper function was called:
$ go test .
--- FAIL: TestThing (0.00s)
scratch_test.go:6: 1 != 2