Go Table-Driven Tests: Test More with Less Code
Instead of copy-pasting tests, define inputs and expected outputs in a table (a slice or map) and loop through them. This is the idiomatic Go way to test functions with many edge cases. The main footgun is a closure bug in parallel tests; re-shadow the.
WHY IT EXISTS Writing a separate test function for every input and expected output is repetitive and hard to maintain. If you find yourself copy-pasting test code just to change the input values, you're creating technical debt. Table-driven tests were adopted as a Go idiom to solve this exact problem of repetition.
THE MENTAL MODEL Think of it like a spreadsheet for your tests. Each row is a complete test case with columns for "name", "input", and "expected output". A single piece of test logic then iterates through each row, runs the function with the given input, and checks if the actual result matches the expected one. You write the test logic once and amortize it across dozens of cases.
HOW IT WORKS You start by defining a struct that represents a single test case, for example, struct { name string; input int; want int }. Then, you declare a slice of these structs, populating it with all your edge cases. Your main test function, TestMyFunction, will contain a single loop that iterates over this slice. Inside the loop, you use t.Run(testCase.name, ...) to create a distinct subtest for each case. This ensures that test failures clearly identify which case failed, making debugging much faster.
WHEN TO USE IT This pattern is the default for testing functions in Go. It's especially powerful for functions with clear input-output behavior that need to be checked against many edge cases. Use it for testing mathematical functions, string manipulation, parsers, or any pure function. It makes adding new test cases trivial—you just add a new entry to the table.
WHEN NOT TO USE IT While versatile, the pattern can become clumsy for tests requiring complex, unique setup or teardown for each case. If every test case needs a different mock object, a database connection in a specific state, or other complex environmental setup, writing separate test functions might be clearer than trying to cram that logic into the table structure.
ONE CANONICAL EXAMPLE A classic use case is testing a string formatting function. The fmt package itself uses this. You'd define a table where each entry has an input format string (like "%-1.2a") and its expected output ("[%-1.2a]"). The test loops through these, calls Sprintf on the input, and uses t.Errorf to report any mismatch with the expected output, like t.Errorf("got %q, want %q", got, tc.want).
Read the original → go.dev
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.