skills/cockroachdb/cockroach/table-driven-test

table-driven-test

SKILL.md

Table-Driven Test Guidelines

Table-driven tests define multiple test cases in a slice of structs, then iterate over them executing the same test logic. This makes it easy to add cases, improves readability, and reduces duplication.

When to use table-driven tests:

  • You have 3+ similar test cases that vary by inputs/outputs
  • Tests follow the same logic pattern with different data
  • Most unit and integration tests benefit from this structure

When to skip:

  • Only 1-2 simple test cases (overhead not worth it)
  • Each test requires completely different logic
  • Test setup/teardown varies significantly between cases

Not the same as: Datadriven tests (different library with testdata files)

Basic Structure

func TestMyFunction(t *testing.T) {
    tests := []struct {
        name        string
        input       string
        expectedLen int
    }{
        {name: "basic case", input: "hello", expectedLen: 5},
        {name: "empty input", input: "", expectedLen: 0},
    }

    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            result, err := MyFunction(tc.input)
            require.NoError(t, err)
            require.Equal(t, tc.expectedLen, result)
        })
    }
}

Core Principles

1. Only Specify What's Necessary

Bad:

{
    name: "remap table",
    tableID: 100, tableName: "users", schemaID: 1,
    schemaName: "public", databaseID: 50, databaseName: "mydb",
    expectedID: 51,  // only this is actually tested!
}

Good:

{name: "remap table", tableID: 100, expectedID: 51}

2. Struct Field Ordering: Inputs First, Then Expected

Order struct fields with input fields at the top and verification fields at the bottom. Prefix all verification fields with expected so readers can immediately distinguish inputs from outputs.

Bad:

tests := []struct {
    name      string
    wantErr   bool       // verification mixed with inputs
    input     string
    output    int        // unclear if this is input or verification
}

Good:

tests := []struct {
    name          string
    input         string
    expectedCount int
    expectedErr   string
}

3. One Concern Per Test Case

Bad:

{
    name: "multiple behaviors",
    input: map[int]int{
        10: 60,  // normal remapping
        49: 99,  // edge case
        50: 50,  // system table preservation
    },
}

Good:

{name: "normal remapping", input: map[int]int{10: 60}},
{name: "preserve system table IDs under 50", input: map[int]int{49: 49}},
{name: "remap IDs at or above 50", input: map[int]int{50: 100}},

4. Independent Test Cases

Each case should be self-contained. Don't build dependent state across cases.

5. Names Describe Intent, Not Inputs

Test case names should hint at the intention or scenario, not duplicate the input data. A reader should understand what the case is testing from the name alone.

  • Good: "two matched regions", "error on negative input"
  • Bad: "match region x and y", "test1", "input_abc"

The name should answer "what scenario is this?" not "what data does this use?"

Assertions

Use require.* (stops on failure) for most checks. Use assert.* (continues on failure) only when you want to see multiple failures.

Common patterns:

// Errors
require.NoError(t, err)
require.Error(t, err)
require.ErrorContains(t, err, "not found")

// Equality
require.Equal(t, expected, actual)  // shows both values on failure
require.True(t, result == expected) // don't do this - hides values

// Collections
require.Len(t, slice, expectedLen)
require.Contains(t, slice, element)

Avoid redundant nil checks: Don't use require.NotNil before an assertion that will already fail on nil (like require.Equal or require.Contains). The subsequent assertion provides a clearer failure message anyway.

// Bad: redundant nil check
require.NotNil(t, result)
require.Equal(t, expectedVal, result.Field)

// Good: Equal already fails clearly if result is nil
require.Equal(t, expectedVal, result.Field)

Error handling in test cases:

tests := []struct {
    name        string
    input       string
    expectedErr string
}{
    {name: "valid input", input: "hello"},
    {name: "empty input rejected", input: "", expectedErr: "must not be empty"},
}

for _, tc := range tests {
    t.Run(tc.name, func(t *testing.T) {
        err := Validate(tc.input)
        if tc.expectedErr != "" {
            require.ErrorContains(t, err, tc.expectedErr)
        } else {
            require.NoError(t, err)
        }
    })
}

Variadic Helper Functions

Use helpers to reduce boilerplate and make test data readable.

Example from CockroachDB (pkg/backup/compaction_dist_test.go):

// Helper types
type mockEntry struct {
    span     roachpb.Span
    locality string
}

// Variadic helpers
func entry(start, end string, locality string) mockEntry {
    return mockEntry{
        span:     mockSpan(start, end),
        locality: locality,
    }
}

func entries(specs ...mockEntry) []execinfrapb.RestoreSpanEntry {
    var entries []execinfrapb.RestoreSpanEntry
    for _, s := range specs {
        var dir cloudpb.ExternalStorage
        if s.locality != "" {
            dir = cloudpb.ExternalStorage{
                URI: "nodelocal://1/test?COCKROACH_LOCALITY=" + s.locality,
            }
        }
        entries = append(entries, execinfrapb.RestoreSpanEntry{
            Span:  s.span,
            Files: []execinfrapb.RestoreFileSpec{{Dir: dir}},
        })
    }
    return entries
}

// Usage - reads like a specification
entries := entries(
    entry("a", "b", "dc=dc1"),
    entry("c", "d", "dc=dc2"),
    entry("e", "f", "dc=dc3"),
)

When to create helpers:

  • Complex struct initialization obscures test intent
  • Patterns repeat across test cases
  • Building composite data structures

When NOT to use:

  • Simple values that don't need transformation
  • One-off test cases
  • Helpers add more complexity than they remove

Integration Tests

For tests requiring a database server, see the /integration-test skill. The table-driven patterns here apply to both unit and integration tests.

Weekly Installs
26
GitHub Stars
32.0K
First Seen
Feb 25, 2026
Installed on
opencode26
gemini-cli26
github-copilot26
codex26
kimi-cli26
amp26