Golang Table Test Example
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