Table-driven tests are the idiomatic way to write tests in Go. They reduce duplication and make it easy to add test cases.
Table-driven tests are preferred because they reduce duplication (one test function handles multiple cases), are easy to extend (just add entries to the table), have a clear structure (all test cases visible at once), and are consistent (same test logic for all cases).
Basic table-driven test
package main
import "testing"
func TestAdd(t *testing.T) {
tests := []struct {
name string
a int
b int
expected int
}{
{
name: "positive numbers",
a: 2,
b: 3,
expected: 5,
},
{
name: "negative numbers",
a: -2,
b: -3,
expected: -5,
},
{
name: "zero",
a: 0,
b: 0,
expected: 0,
},
{
name: "mixed signs",
a: 5,
b: -3,
expected: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
t.Run() creates a subtest. Each subtest can be run independently:
go test -run TestAdd/positive # Run only "positive numbers" subtest
Subtests are useful because they enable selective execution (run specific test cases), parallel execution (subtests can run in parallel), better organization (group related tests), and clearer failures (failures show which subtest failed).
Advanced table-driven test
func TestDivide(t *testing.T) {
tests := []struct {
name string
a float64
b float64
want float64
wantError bool
}{
{
name: "normal division",
a: 10.0,
b: 2.0,
want: 5.0,
wantError: false,
},
{
name: "division by zero",
a: 10.0,
b: 0.0,
want: 0.0,
wantError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Divide(tt.a, tt.b)
if tt.wantError {
if err == nil {
t.Error("expected error, got nil")
}
return
}
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
if got != tt.want {
t.Errorf("Divide(%f, %f) = %f; want %f",
tt.a, tt.b, got, tt.want)
}
})
}
}