Testing Created: 2024-01-27 Updated: 2024-01-27

Table-Driven Tests

A testing pattern that defines test cases as data in a table, enabling comprehensive and maintainable test coverage.

Contributors

When to Use

  • When testing a function with multiple input/output combinations
  • When test cases share the same structure and assertions
  • When you want to easily add new test cases

When NOT to Use

  • For tests requiring significantly different setup or assertions
  • When a single test case is sufficient
  • For integration tests with complex state management

Table-driven tests are the idiomatic way to write tests in Go. Test cases are defined as a slice of structs, and a single loop runs each case. This makes tests easy to read, extend, and maintain.

Implementation

package math

func Abs(n int) int {
    if n < 0 {
        return -n
    }
    return n
}

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

Usage

package math

import (
    "errors"
    "testing"
)

func TestAbs(t *testing.T) {
    tests := []struct {
        name  string
        input int
        want  int
    }{
        {"positive", 5, 5},
        {"negative", -5, 5},
        {"zero", 0, 0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Abs(tt.input)
            if got != tt.want {
                t.Errorf("Abs(%d) = %d, want %d", tt.input, got, tt.want)
            }
        })
    }
}

func TestDivide(t *testing.T) {
    tests := []struct {
        name    string
        a, b    int
        want    int
        wantErr bool
    }{
        {"positive division", 10, 2, 5, false},
        {"negative dividend", -10, 2, -5, false},
        {"division by zero", 10, 0, 0, true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := Divide(tt.a, tt.b)
            if (err != nil) != tt.wantErr {
                t.Errorf("Divide() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if got != tt.want {
                t.Errorf("Divide(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want)
            }
        })
    }
}

Benefits

  • Easy to extend: Adding a test case is just adding a struct to the slice
  • Consistent structure: All cases follow the same pattern
  • Clear naming: Each case has a descriptive name shown in test output
  • Parallel-friendly: Cases can easily be run in parallel with t.Parallel()
  • Reduced duplication: Test logic is written once and reused