0
0
GoHow-ToBeginner · 4 min read

How to Write Table Driven Tests in Go: Simple Guide

In Go, write a table driven test by defining a slice of test cases with inputs and expected outputs, then loop over them in a test function using t.Run for subtests. This approach keeps tests organized, easy to read, and scalable.
📐

Syntax

A table driven test in Go uses a slice of structs to hold test cases. Each struct contains input values and the expected result. The test function loops over this slice, running each case as a subtest with t.Run.

  • testCases: slice holding all test cases
  • name: unique name for each subtest
  • input: data to test
  • expected: expected output
  • t.Run: runs each test case separately
go
func TestFunction(t *testing.T) {
    testCases := []struct {
        name     string
        input    int
        expected int
    }{
        {"case1", 1, 2},
        {"case2", 2, 4},
    }

    for _, tc := range testCases {
        tc := tc // capture variable
        t.Run(tc.name, func(t *testing.T) {
            got := FunctionUnderTest(tc.input)
            if got != tc.expected {
                t.Errorf("got %d, want %d", got, tc.expected)
            }
        })
    }
}
💻

Example

This example tests a simple function that doubles an integer. It shows how to define test cases, loop over them, and check results with t.Errorf.

go
package main

import "testing"

func Double(x int) int {
    return x * 2
}

func TestDouble(t *testing.T) {
    testCases := []struct {
        name     string
        input    int
        expected int
    }{
        {"double 1", 1, 2},
        {"double 2", 2, 4},
        {"double 0", 0, 0},
        {"double negative", -3, -6},
    }

    for _, tc := range testCases {
        tc := tc // capture variable
        t.Run(tc.name, func(t *testing.T) {
            got := Double(tc.input)
            if got != tc.expected {
                t.Errorf("Double(%d) = %d; want %d", tc.input, got, tc.expected)
            }
        })
    }
}
Output
PASS ok command-line-arguments 0.001s
⚠️

Common Pitfalls

Common mistakes include:

  • Not using t.Run which makes it harder to identify failing cases.
  • Reusing loop variables inside subtests without capturing them, causing all subtests to use the last value.
  • Not giving descriptive names to test cases.

Always capture the loop variable inside the loop to avoid closure issues.

go
func TestWrong(t *testing.T) {
    testCases := []struct {
        name  string
        input int
    }{
        {"case1", 1},
        {"case2", 2},
    }

    for _, tc := range testCases {
        // Wrong: using tc directly in goroutine causes all tests to use last tc
        t.Run(tc.name, func(t *testing.T) {
            // test code
        })
    }
}

func TestRight(t *testing.T) {
    testCases := []struct {
        name  string
        input int
    }{
        {"case1", 1},
        {"case2", 2},
    }

    for _, tc := range testCases {
        tc := tc // capture variable
        t.Run(tc.name, func(t *testing.T) {
            // test code
        })
    }
}
📊

Quick Reference

  • Define test cases as a slice of structs with descriptive names.
  • Use t.Run to run each case as a subtest.
  • Capture loop variables inside the loop to avoid closure bugs.
  • Check results with t.Errorf or t.Fatalf.
  • Keep tests simple and readable for easy maintenance.

Key Takeaways

Use a slice of structs to hold test cases with inputs and expected outputs.
Run each test case as a subtest using t.Run for clear, isolated results.
Always capture loop variables inside the loop to avoid closure bugs.
Give each test case a descriptive name for easier debugging.
Table driven tests make your Go tests organized and scalable.