8.2 Table-Driven Tests

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)
            }
        })
    }
}