Skip to content
Theory · From a file to a project · lesson 7 of 10

Table tests and t.Run

In the previous lesson the test checked one call of Clamp. But a function usually has many interesting cases: a negative number, zero, a boundary, a very large value. If every case gets its own if with t.Errorf, the test grows fast, and the cases get lost among identical lines.

In a factory this is solved like so: the inspector keeps an inspection sheet for each part. The sheet says which part it is, what goes in and what should come out. In Go the stack of sheets is a table of cases, and t.Run gives each part a sheet of its own.

We will test the Abs function of the abs package, which returns the absolute value of a number:

GO
package abs

func Abs(x int) int {
    if x < 0 {
        return -x
    }
    return x
}

A table of cases

A table test is a slice of cases and one loop that checks them one by one. Here is the test for Abs in abs_test.go, with the same package abs and import "testing" lines at the top:

GO
func TestAbs(t *testing.T) {
    cases := []struct {
        name    string
        x, want int
    }{
        {"negative", -7, 7}, {"zero", 0, 0}, {"positive", 4, 4},
    }
    for _, c := range cases {
        t.Run(c.name, func(t *testing.T) {
            if got := Abs(c.x); got != c.want {
                t.Errorf("Abs(%d) = %d, want %d", c.x, got, c.want)
            }
        })
    }
}

Let's take it apart:

  1. []struct { … } is a slice of structs with no type name. Each struct is one row of the table.
  2. The fields of a row: name is the case name, x the input, want the expected result.
  3. {"negative", -7, 7} is one row: the case "negative", input -7, expecting 7.
  4. The for loop goes over all rows and calls Abs(c.x) for each.

A new case is one new row of the table, and the checking code stays the same. That way the test grows by rows, not by copies of if.

t.Run and subtests

t.Run(c.name, func(t *testing.T) { … }) runs the function as a separate subtest named after the case. This is what it gives you:

  • the subtest has its own name in the report: TestAbs/negative;
  • a subtest passes or fails on its own, and one failing case does not hide the others;
  • inside the function there is its own t parameter, and t.Fatalf through it stops only this subtest — you saw that in the previous lesson's widget.

Here is what go test -v prints:

TEXT
$ go test -v
=== RUN   TestAbs
=== RUN   TestAbs/negative
=== RUN   TestAbs/zero
=== RUN   TestAbs/positive
--- PASS: TestAbs (0.00s)
    --- PASS: TestAbs/negative (0.00s)
    --- PASS: TestAbs/zero (0.00s)
    --- PASS: TestAbs/positive (0.00s)
PASS
ok  	example.com/abs	0.003s

Each case got its own === RUN line and its own --- PASS result. Were Abs wrong on negative numbers, the report would have a --- FAIL: TestAbs/negative line, and zero and positive would still pass.

One subtest on its own

The -run flag runs only the tests whose name matches a pattern. A subtest's name is written after a slash:

TEXT
$ go test -v -run TestAbs/negative
=== RUN   TestAbs
=== RUN   TestAbs/negative
--- PASS: TestAbs (0.00s)
    --- PASS: TestAbs/negative (0.00s)
PASS
ok  	example.com/abs	0.004s

This helps when you are fixing one case: no waiting for the others and no hunting for the right line in a long report. Interviewers ask about table tests and t.Run often: most Go tests are written this way.

This is where the inspection-sheet picture stops working. In a factory the inspector writes the part's name on the sheet and would never allow two identical sheets. In Go a subtest's name is the name string from the table, and Go does not require the names to differ: a repeated name is quietly renamed.

Common mistake

A row of the table was copied, and its name was never changed:

GO
{"negative", -7, 7}, {"zero", 0, 0}, {"zero", 4, 4}, // ✗
TEXT
=== RUN   TestAbs
=== RUN   TestAbs/negative
=== RUN   TestAbs/zero
=== RUN   TestAbs/zero#01
--- PASS: TestAbs (0.00s)
    --- PASS: TestAbs/negative (0.00s)
    --- PASS: TestAbs/zero (0.00s)
    --- PASS: TestAbs/zero#01 (0.00s)
PASS
ok  	example.com/abs	0.004s

The test passes, but go test quietly renamed the second case to zero#01. If that case fails, the report does not say which row it is, and -run TestAbs/zero runs both. Give every case its own name, such as positive.

Key points

  • A table test is a slice of structs with a name field, an input and the expected result, plus one loop.
  • t.Run(c.name, func(t *testing.T) { … }) makes each case a separate subtest, TestAbs/negative.
  • A subtest fails on its own, and t.Fatalf inside it stops only that subtest.
  • go test -run TestAbs/negative runs one subtest; case names must be distinct.

Subscribe to take the course

The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.

Sign in another way