Introduction to Go sub tests
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. Thestring
key represents the name of the test case. And thestruct
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
usingt.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? ✌️