Introduction to Go sub tests

Sebastian Dahlgren
Saltside Engineering
2 min readNov 1, 2017

Introduction

I’ve been using the table based tests pattern in Go for a long time and it has served my purposes well. We are also using it extensively at Saltside. A common code review comment I give is to use the sub test pattern in Go. So I’d like to walk you through that pattern in this blog post.

It’s really very simple to adapt if you’re already using table based tests.

A basic table based test

A typical table based test without sub tests would look something like the listing below.

package somethingimport "github.com/stretchr/testify/assert"func TestMultiply(t *testing.T) {
tests := map[string]struct{
x int
y int
expected int
}{
"x less than y": { x: 1, y: 2, expected: 2 },
"y less than x": { x: 2, y: 1, expected: 2 },
"negative multiplication": { x: 2, y: -1: expected: -2 },
}
for name, test := range tests {
t.Logf("Running test %s", name)
result := multiply(test.x, test.y)
assert.Equal(t, test.expected, result)
}
}

So, in brief, this is structure of our test:

  • We have a map[string]struct that defines out test cases. The string key represents the name of the test case. And the struct holds test input and expectations.
  • Then we loop over these test cases one by one.
  • For each test case we print the name of the test to stdout using t.Logf.
  • Then we run the actual test.

The output looks something like this:

=== RUN   TestMultiply
--- PASS: TestMultiply (0.00s)
multiply_test.go:17: Running test case x less than y
multiply_test.go:17: Running test case y less than x
multiply_test.go:17: Running test case negative multiplication
PASS

This is alright, we can at least see which test cases are being run. And if there is a failure we can see which test case was failing.

But we can not see clearly on an individual test case level whether they passed or failed. Nor can we see how much time we spent on each individual test case. These things are currently only represented on the TestMultiply level. As an aggregate over all our test cases. 🤔

Converting the test to a sub test 🏁

Let’s take the test code we had above and modify it just a little.

package somethingimport "github.com/stretchr/testify/assert"func TestMultiply(t *testing.T) {
tests := map[string]struct{
x int
y int
expected int
}{
"x less than y": { x: 1, y: 2, expected: 2 },
"y less than x": { x: 2, y: 1, expected: 2 },
"negative multiplication": { x: 2, y: -1: expected: -2 },
}
for name, test := range tests {
t.Run(name, func(t *testing.T){
result := multiply(test.x, test.y)
assert.Equal(t, test.expected, result)
})
}
}

Notice how we removed the t.Logf statement printing the test name. We are also calling t.Run with the test name and an anonymous function with a nested *testing.T.

go test will now execute these tests, evaluate their outcome and measure their time individually.

Let’s see what the output is like now!

--- PASS: TestMultiply (0.00s)
--- PASS: TestMultiply/y_less_than_x (0.00s)
--- PASS: TestMultiply/x_less_than_y (0.00s)
--- PASS: TestMultiply/negative_multiplication (0.00s)

Waaaay nicer, right? ✌️

--

--

Sebastian Dahlgren
Saltside Engineering

Developer and Go ❤. Love code. And clouds. Nerd on a mission.