Writing Table-Driven Tests in Go
Table-driven tests are a powerful testing pattern in Go that allows you to test multiple scenarios with a single test function. This guide covers how to write and organize table-driven tests effectively.
Basic Table Tests
Simple Example
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -2, -3, -5},
{"zero values", 0, 0, 0},
{"mixed signs", -2, 3, 1},
}
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)
}
})
}
}
Test Case Structure
type testCase struct {
name string // Test case name
input string // Input value
expected string // Expected output
wantErr bool // Whether an error is expected
setupFunc func() error // Optional setup function
cleanupFunc func() // Optional cleanup function
}
func TestProcess(t *testing.T) {
tests := []testCase{
{
name: "valid input",
input: "hello",
expected: "HELLO",
wantErr: false,
},
{
name: "empty input",
input: "",
expected: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.setupFunc != nil {
if err := tt.setupFunc(); err != nil {
t.Fatal(err)
}
}
if tt.cleanupFunc != nil {
defer tt.cleanupFunc()
}
result, err := Process(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("Process() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err == nil && result != tt.expected {
t.Errorf("Process() = %v, want %v", result, tt.expected)
}
})
}
}
Advanced Patterns
Testing Complex Types
func TestUserValidation(t *testing.T) {
tests := []struct {
name string
user User
wantErr error
checks []func(User) bool
}{
{
name: "valid user",
user: User{
Name: "John Doe",
Email: "[email protected]",
Age: 30,
},
wantErr: nil,
checks: []func(User) bool{
func(u User) bool { return u.Name != "" },
func(u User) bool { return u.Age >= 18 },
},
},
{
name: "invalid email",
user: User{
Name: "John Doe",
Email: "invalid-email",
Age: 30,
},
wantErr: ErrInvalidEmail,
checks: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.user.Validate()
// Check error
if !errors.Is(err, tt.wantErr) {
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
// Run additional checks
if err == nil && tt.checks != nil {
for i, check := range tt.checks {
if !check(tt.user) {
t.Errorf("check %d failed for user %+v", i, tt.user)
}
}
}
})
}
}
Testing Multiple Return Values
func TestParse(t *testing.T) {
tests := []struct {
name string
input string
wantKey string
wantValue int
wantVersion string
wantErr bool
}{
{
name: "valid input",
input: "key=123;v1.0",
wantKey: "key",
wantValue: 123,
wantVersion: "v1.0",
wantErr: false,
},
{
name: "invalid format",
input: "invalid",
wantKey: "",
wantValue: 0,
wantVersion: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
key, value, version, err := Parse(tt.input)
// Check error
if (err != nil) != tt.wantErr {
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Check return values
if key != tt.wantKey {
t.Errorf("key = %v, want %v", key, tt.wantKey)
}
if value != tt.wantValue {
t.Errorf("value = %v, want %v", value, tt.wantValue)
}
if version != tt.wantVersion {
t.Errorf("version = %v, want %v", version, tt.wantVersion)
}
})
}
}
Best Practices
1. Organize Test Cases
// Group related test cases
var stringTests = []struct {
name string
input string
expected string
}{
// Basic cases
{"empty string", "", ""},
{"single char", "a", "A"},
// Special characters
{"with spaces", "hello world", "HELLO WORLD"},
{"with symbols", "hello!", "HELLO!"},
// Unicode
{"unicode", "café", "CAFÉ"},
}
// Use constants for repeated values
const (
validEmail = "[email protected]"
invalidEmail = "invalid-email"
)
var userTests = []struct{
// Test case definition
}
2. Use Helper Functions
func TestComplex(t *testing.T) {
tests := []struct {
name string
setup func(t *testing.T) (string, func())
validate func(t *testing.T, result string)
}{
{
name: "complex test",
setup: func(t *testing.T) (string, func()) {
t.Helper()
// Setup code
return "test data", func() {
// Cleanup code
}
},
validate: func(t *testing.T, result string) {
t.Helper()
if result != "expected" {
t.Errorf("got %q, want %q", result, "expected")
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
input, cleanup := tt.setup(t)
defer cleanup()
result := ComplexFunction(input)
tt.validate(t, result)
})
}
}
3. Clear Error Messages
func TestWithContext(t *testing.T) {
tests := []struct {
name string
input string
want string
wantErr bool
}{
// Test cases
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Process(tt.input)
// Detailed error messages
if (err != nil) != tt.wantErr {
t.Errorf("%s: Process(%q) error = %v, wantErr = %v",
tt.name, tt.input, err, tt.wantErr)
}
if got != tt.want {
t.Errorf("%s: Process(%q) = %q, want %q",
tt.name, tt.input, got, tt.want)
}
})
}
}
4. Table Organization
func TestFeature(t *testing.T) {
// Define types for better organization
type args struct {
input string
options Options
timeout time.Duration
}
type want struct {
result string
err error
metadata Metadata
}
tests := []struct {
name string
args args
want want
}{
{
name: "successful case",
args: args{
input: "test",
options: DefaultOptions(),
timeout: time.Second,
},
want: want{
result: "processed",
err: nil,
metadata: Metadata{Valid: true},
},
},
// More test cases...
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, metadata, err := Feature(
tt.args.input,
tt.args.options,
tt.args.timeout,
)
if !reflect.DeepEqual(result, tt.want.result) {
t.Errorf("result = %v, want %v", result, tt.want.result)
}
if !reflect.DeepEqual(metadata, tt.want.metadata) {
t.Errorf("metadata = %v, want %v", metadata, tt.want.metadata)
}
if !errors.Is(err, tt.want.err) {
t.Errorf("error = %v, want %v", err, tt.want.err)
}
})
}
}
Next Steps
- Learn about Benchmarking
- Explore Mocking
- Study Test Coverage