Table-Driven Tests
Jest it.each vs Go table-driven tests
Section titled “Jest it.each vs Go table-driven tests”In Jest you reach for it.each or describe.each when you want to run the same assertion logic over many inputs without copy-pasting test functions. Go has no built-in it.each — instead, the community settled on a universal idiom: a slice of anonymous structs (the “table”), looped inside a single test function, each case run as a subtest via t.Run.
Playground note: The
testingpackage requires thego testbinary context. Code blocks below show real test files — run them locally withgo test ./....
Comparison: testing the same function
Section titled “Comparison: testing the same function”// Jest — it.eachfunction add(a: number, b: number) { return a + b; }
it.each([ [1, 2, 3], [0, 0, 0], [-1, 1, 0], [10, 20, 30],])('add(%i, %i) = %i', (a, b, want) => { expect(add(a, b)).toBe(want);});// Go — table-driven with t.Runfunc add(a, b int) int { return a + b }
func TestAdd(t *testing.T) { tests := []struct { name string a, b int want int }{ {"positive", 1, 2, 3}, {"zeros", 0, 0, 0}, {"negative", -1, 1, 0}, {"large", 10, 20, 30}, }
for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { got := add(tc.a, tc.b) if got != tc.want { t.Errorf("add(%d, %d) = %d; want %d", tc.a, tc.b, got, tc.want) } }) }}Anatomy of a table-driven test
Section titled “Anatomy of a table-driven test”package math_test
import "testing"
func TestDivide(t *testing.T) { // 1. The table — a slice of anonymous structs tests := []struct { name string a, b float64 want float64 wantErr bool }{ {"normal", 10, 2, 5, false}, {"fraction", 7, 2, 3.5, false}, {"divide_by_zero", 1, 0, 0, true}, }
// 2. Loop over the table for _, tc := range tests { // 3. t.Run creates a named subtest t.Run(tc.name, func(t *testing.T) { // 4. t.Parallel() — optional, runs subtests concurrently t.Parallel()
got, err := Divide(tc.a, tc.b)
if (err != nil) != tc.wantErr { t.Fatalf("Divide() error = %v, wantErr %v", err, tc.wantErr) } if !tc.wantErr && got != tc.want { t.Errorf("Divide() = %v; want %v", got, tc.want) } }) }}Key conventions:
t.Errorfmarks failure but continues the test.t.Fatalfmarks failure and stops the current subtest immediately.t.Parallel()insidet.Runallows subtests to run concurrently (safe when cases are independent).- The
namefield in each case becomes the subtest label (TestDivide/normal,TestDivide/divide_by_zero).
Running specific subtests
Section titled “Running specific subtests”# Run all testsgo test ./...
# Run only one subtest by name (supports regex)go test -run TestDivide/divide_by_zero ./...
# Run all subtests whose name contains "zero"go test -run TestDivide/.*zero.* ./...
# Verbose output — prints each subtest name and PASS/FAILgo test -v ./...Comparing to Jest structure
Section titled “Comparing to Jest structure”// Jestdescribe('Divide', () => { it.each([ ['normal', 10, 2, 5], ['fraction', 7, 2, 3.5], ])('%s', (_, a, b, want) => { expect(divide(a, b)).toBe(want); });
it('throws on divide by zero', () => { expect(() => divide(1, 0)).toThrow(); });});// Gofunc TestDivide(t *testing.T) { tests := []struct { name string a, b float64 want float64 wantErr bool }{ {"normal", 10, 2, 5, false}, {"fraction", 7, 2, 3.5, false}, {"by_zero", 1, 0, 0, true}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { got, err := Divide(tc.a, tc.b) if (err != nil) != tc.wantErr { t.Fatal(...) } if !tc.wantErr && got != tc.want { t.Errorf(...) } }) }}