How to Write Table Driven Tests in Go: Simple Guide
In Go, write a
table driven test by defining a slice of test cases with inputs and expected outputs, then loop over them in a test function using t.Run for subtests. This approach keeps tests organized, easy to read, and scalable.Syntax
A table driven test in Go uses a slice of structs to hold test cases. Each struct contains input values and the expected result. The test function loops over this slice, running each case as a subtest with t.Run.
- testCases: slice holding all test cases
- name: unique name for each subtest
- input: data to test
- expected: expected output
- t.Run: runs each test case separately
go
func TestFunction(t *testing.T) {
testCases := []struct {
name string
input int
expected int
}{
{"case1", 1, 2},
{"case2", 2, 4},
}
for _, tc := range testCases {
tc := tc // capture variable
t.Run(tc.name, func(t *testing.T) {
got := FunctionUnderTest(tc.input)
if got != tc.expected {
t.Errorf("got %d, want %d", got, tc.expected)
}
})
}
}Example
This example tests a simple function that doubles an integer. It shows how to define test cases, loop over them, and check results with t.Errorf.
go
package main import "testing" func Double(x int) int { return x * 2 } func TestDouble(t *testing.T) { testCases := []struct { name string input int expected int }{ {"double 1", 1, 2}, {"double 2", 2, 4}, {"double 0", 0, 0}, {"double negative", -3, -6}, } for _, tc := range testCases { tc := tc // capture variable t.Run(tc.name, func(t *testing.T) { got := Double(tc.input) if got != tc.expected { t.Errorf("Double(%d) = %d; want %d", tc.input, got, tc.expected) } }) } }
Output
PASS
ok command-line-arguments 0.001s
Common Pitfalls
Common mistakes include:
- Not using
t.Runwhich makes it harder to identify failing cases. - Reusing loop variables inside subtests without capturing them, causing all subtests to use the last value.
- Not giving descriptive names to test cases.
Always capture the loop variable inside the loop to avoid closure issues.
go
func TestWrong(t *testing.T) {
testCases := []struct {
name string
input int
}{
{"case1", 1},
{"case2", 2},
}
for _, tc := range testCases {
// Wrong: using tc directly in goroutine causes all tests to use last tc
t.Run(tc.name, func(t *testing.T) {
// test code
})
}
}
func TestRight(t *testing.T) {
testCases := []struct {
name string
input int
}{
{"case1", 1},
{"case2", 2},
}
for _, tc := range testCases {
tc := tc // capture variable
t.Run(tc.name, func(t *testing.T) {
// test code
})
}
}Quick Reference
- Define test cases as a slice of structs with descriptive names.
- Use
t.Runto run each case as a subtest. - Capture loop variables inside the loop to avoid closure bugs.
- Check results with
t.Errorfort.Fatalf. - Keep tests simple and readable for easy maintenance.
Key Takeaways
Use a slice of structs to hold test cases with inputs and expected outputs.
Run each test case as a subtest using t.Run for clear, isolated results.
Always capture loop variables inside the loop to avoid closure bugs.
Give each test case a descriptive name for easier debugging.
Table driven tests make your Go tests organized and scalable.