Golang Table Test Example
Alex Garella
5 May 2023
In this article we are going to explain how you can improve your test structure in Go (Golang) by using table-driven tests. This is something that will simplify tedious and repetitive unit tests in Go (Golang) when testing functions with multiple scenarios and data. In order to write table-driven tests in Go (Golang) you don’t need any particular library, everything can be done just by using the built-in standard library.
Before we start I will provide an example that should be unit-tested, I will first present a standard unit test structure and then refactor that to use table-driven tests, or table tests.
Golang Unit Testing Example
In this example we have a function that we want to test, this function is called SliceIntersect. The function takes two string slices and tries to find all elements that can be found in both the first and the second slice. It will return a slice that contains common elements. Let’s see our function first.
src/main.go
func SliceIntersect(a, b []string) []string {
aMap := make(map[string]struct{}, len(a))
for _, el := range a {
aMap[el] = struct{}{}
}
ret := make([]string, 0)
for _, el := range b {
if _, found := aMap[el]; found {
ret = append(ret, el)
}
}
return ret
} Now let’s write a normal set of unit tests for this function. Without using table-driven tests.
src/main_test.go
package main
import "testing"
func TestSliceIntersectOneElement(t *testing.T) {
out := SliceIntersect([]string{"a"}, []string{"a"})
if len(out) != 1 && out[0] != "a" {
t.Errorf("test failed")
}
}
func TestSliceIntersectMultipleElements(t *testing.T) {
out := SliceIntersect([]string{"a", "b", "c", "h"}, []string{"c", "a", "z", "f"})
if len(out) != 2 && out[0] != "c" && out[1] != "a" {
t.Errorf("test failed")
}
}
func TestSliceIntersectEmpty(t *testing.T) {
out := SliceIntersect([]string{}, []string{})
if len(out) > 0 {
t.Errorf("test failed")
}
}
func TestSliceIntersectOneEmpty(t *testing.T) {
out := SliceIntersect([]string{}, []string{"a"})
if len(out) > 0 {
t.Errorf("test failed")
}
} As you can see writing new test cases it’s pretty tedious and time consuming as you will have to create a new function each time and pass in new arguments as you need them.
Golang Table Driven Testing Example
Now let’s see how we can re-arrange the above example to use a table-driven test. The first thing we are going to do is create a single function, create a slice of structs that holds our test cases, and then iterate over that slice to run all the test cases one by one.
src/main_test.go
package main
import "testing"
func TestSliceIntersect(t *testing.T) {
testCases := []struct {
name string
a []string
b []string
expected []string
}{
{
name: "intersect one",
a: []string{"a"},
b: []string{"a"},
expected: []string{"a"},
},
{
name: "intersect multiple elements",
a: []string{"a", "b", "c", "h"},
b: []string{"c", "h", "z", "f"},
expected: []string{"c", "h"},
},
{
name: "intersect empty",
a: []string{},
b: []string{},
expected: []string{},
},
{
name: "intersect one empty",
a: []string{"a"},
b: []string{},
expected: []string{},
},
}
for _, tc := range testCases {
out := SliceIntersect(tc.a, tc.b)
if len(out) != len(tc.expected) {
t.Errorf("test failed expected len")
}
for i := 0; i < len(out); i++ {
if out[i] != tc.expected[i] {
t.Errorf("test failed expected element")
}
}
}
} It’s also possible in Go (Golang) to run tests in parallel by using the testing.Run and testing.Parallel methods, so the example will be as follows:
package main
import "testing"
func TestSliceIntersect(t *testing.T) {
testCases := []struct {
name string
a []string
b []string
expected []string
}{
{
name: "intersect one",
a: []string{"a"},
b: []string{"a"},
expected: []string{"a"},
},
{
name: "intersect multiple elements",
a: []string{"a", "b", "c", "h"},
b: []string{"c", "h", "z", "f"},
expected: []string{"c", "h"},
},
{
name: "intersect empty",
a: []string{},
b: []string{},
expected: []string{},
},
{
name: "intersect one empty",
a: []string{"a"},
b: []string{},
expected: []string{},
},
}
t.Parallel()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
out := SliceIntersect(tc.a, tc.b)
if len(out) != len(tc.expected) {
t.Errorf("test failed expected len")
}
for i := 0; i < len(out); i++ {
if out[i] != tc.expected[i] {
t.Errorf("test failed expected element")
}
}
})
}
} This will run each of the above test cases in different go-routines