1. go
  2. /testing
  3. /table-tests

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