Test Tables in Go

I dove into the time standard library the other week to fix an outstanding bug. Turns out the bug is not quite a bug, but it might be… lets just say its status is complicated. Anyway, while diving through the code I found a new (to me) method for doing repetitive tests. Lets setup a fictitious function to test so I can show you why it’s so awesome.

The function

Lets say the function input is a list of integers. Given this list, return the integer closest to 1 which is not in the list. So that is to say given the list of [1,2,3,5], our function should return 4. Also of note, duplicates may exist, order is not guaranteed, and an empty list should return 1. Got it? Awesome, so here is our function signature:

func LowestAvailableInt(input []int) int

Normal tests

We know the function’s signature, so lets do a little TDD. Here is how I would usually start:

func TestEmptyList(t *testing.T) {
    input := []int{}
    expected := 1
    result := LowestAvailableInt(input)
    if result != expected {
        t.Errorf("Result should have been %d, but it was %d [Input: %#v]", expected, result, input)
    }
}

Run go test and see that it fails. Fix the function implementation by making it return 1 and continue adding more test cases:

...

func TestSingleItem(t *testing.T) {
    input := []int{1}
    expected := 2
    result := LowestAvailableInt(input)
    if result != expected {
        t.Errorf("Result should have been %d, but it was %d [Input: %#v]", expected, result, input)
    }
}

func TestUnorderedItems(t *testing.T) {
    input := []int{5, 1, 4, 2}
    expected := 3
    result := LowestAvailableInt(input)
    if result != expected {
        t.Errorf("Result should have been %d, but it was %d [Input: %#v]", expected, result, input)
    }
}

func TestDuplicateItems(t *testing.T) {
    input := []int{1, 2, 4, 5, 6, 1, 4, 5, 2, 3}
    expected := 7
    result := LowestAvailableInt(input)
    if result != expected {
        t.Errorf("Result should have been %d, but it was %d {Input: %#v}", expected, result, input)
    }
}

As you can imagine, this could drag out for a while depending on the complexity of the function. So how can we improve this repetitive testing process?

Enter test tables

Here’s where it gets awesome. I found this method of testing in the src/pkg/time/time_test.go file. Basically what you do is setup a data structure with your input and expected result. This could be a map or better yet, a slice of structs. Let’s convert the above test cases into a test table:

var testTable = []struct {
    input    []int
    expected int
}{
    {[]int{}, 1},
    {[]int{1}, 2},
    {[]int{5, 1, 4, 2}, 3},
    {[]int{1, 2, 4, 5, 6, 1, 4, 5, 2, 3}, 7},
}

func TestLowestAvailableInt(t *testing.T) {
    for _, data := range testTable {
        if result := LowestAvailableInt(data.input); result != data.expected {
            t.Errorf("Result should have been %d, but it was %d", data.expected, result, data.input)
        }
    }
}

Conclusion

How awesome is that!? This has turned into one of my favorite testing methods in Go. It’s concise, readable, and so easy to extend. If we ever find a new edge case or want to expand our testing, we can just add a new case to our testTable.

...
{[]int{9}, 1},
{[]int{12395768557482757672}, 1},
{[]int{2,2,2,2,3,3,3,3,4,4,4,4}, 1},
...