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

Join the Golang Developers Community on Golang Cafe