Table driven tests is a way to write concise tests which can later be easily extended with new test cases. Table driven tests are common in Go (though not unique to it) and a number of standard libraries¹ have these kind of tests. Table driven tests make use of anonymous structs.

In this article I will show you how to write table driven tests. Continuing with the errline repo, we will introduce tests for the Wrap() function. This function annotates an error with a file and line number at the point where Wrap() was called. In particular, we want to write a test for the logic which computes the short name of the file (highlighted in bold). Originally, the Wrap() function looks like this.

func Wrap(err error) error {

if err == nil {

return nil

}

// If error already has file line do not add it again.

if _, ok := err.(*withFileLine); ok {

return err

} _, file, line, ok := runtime.Caller(calldepth)

if !ok {

file = "???"

line = 0

} short := file

for i := len(file) - 1; i > 0; i-- {

if file[i] == '/' {

short = file[i+1:]

break

}

}

file = short

return &withFileLine{err, file, line}

}

In order to test the short name computation logic, it is convenient to extract that logic into a separate function called getShortFilename() . Now, the code looks like this.

func Wrap(err error) error {

if err == nil {

return nil

}

// If error already has file line do not add it again.

if _, ok := err.(*withFileLine); ok {

return err

} _, file, line, ok := runtime.Caller(calldepth)

if !ok {

file = "???"

line = 0

} file = getShortFilename(file)

return &withFileLine{err, file, line}

} func getShortFilename(file string) string {

short := file

for i := len(file) - 1; i > 0; i-- {

if file[i] == '/' {

short = file[i+1:]

break

}

}

file = short

return file

}

It is pretty common to find ourselves refactoring our code to make it testable.

We will now test the function getShortFilename() by passing various file names and verifying that the output matches the expectation.

We start off with an empty test function

func TestShortFilename(t *testing.T) {

}

Next, we introduce an anonymous struct with two fields in and expected . in represents the input to the getShortFilename() function and expected is the expected return of that function. tests is an array of such structs.

func TestShortFilename(t *testing.T) {

tests := []struct {

in string // input

expected string // expected result

}{

{"???", "???"},

{"filename.go", "filename.go"},

{"hello/filename.go", "filename.go"},

{"main/hello/filename.go", "filename.go"},

}

}

With this, we can run over the test cases in a loop in our test function.

func TestShortFilename(t *testing.T) {

tests := []struct {

in string

expected string

}{

{"???", "???"},

{"filename.go", "filename.go"},

{"hello/filename.go", "filename.go"},

{"main/hello/filename.go", "filename.go"},

} for _, tt := range tests {

actual := getShortFilename(tt.in)

if strings.Compare(actual, tt.expected) != 0 {

t.Fail()

}

}

}

Note, that adding another test case is pretty simply - just add another item in the tests array.

This scheme can be extended to functions which accept and return multiple values.

That is it!

This code can be accessed at my github.

References: