Skip to content

testing: add Name to track file and line of test case declaration #52751

@dsnet

Description

@dsnet

In Go, it is very common to use table-driven tests:

tests := struct {
    name string
    input T
    ...
} {{
    name: "Foo",
    ...,
}, {
    name: "Bar",
    ...,
}
... // maybe dozens or hundreds more cases
}
for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
        got, err := fizz(tt.input)
        if err != nil {
            t.Fatalf("fizz error: %v", err) // my_test.go:1234
        }
    })
}

When this test fails, it prints with something like:

--- FAIL: Test/Bar (0.00s)
    my_test.go:1234: fizz error: the world blew up
  • The my_test.go:1234 tells us where in the test logic this failed.
  • The Test/Bar name tells us which test case failed.

Most code editors today identify source.go:1234 strings and automatically provide the ability to jump to that source code location. This is really helpful for jumping to the execution logic that failed, but is not helpful for jumping to the test data that caused the failure. It is impossible for editor tooling to automatically correlate the the test name (e.g., Test/Bar) with the test case in the code since the association between the two can be determined by arbitrary Turing-complete logic.

I propose the following API in the testing package:

// NameFileLine is a name combined with a file and line number.
type NameFileLine struct { ... }

// Name constructs a NameFileLine.
// It annotates the name with the file and line number of the caller.
func Name(name string) NameFileLine

// RunName runs f as a subtest of t called name.
func (t *T) RunName(name NameFileLine, f func(t *testing.T))

// RunName runs f as a subtest of b called name.
func (b *B) RunName(name NameFileLine, f func(b *testing.B))

Using this API, the example above would be changed as:

  tests := struct {
-     name string
+     name testing.NameFileLine
      input T
      ...
  } {{
-     name: "Foo",
+     name: testing.Name("Foo"),
      ...,
  }, {
-     name: "Bar",
+     name: testing.Name("Bar"), // my_test.go:321
      ...,
  }
  ... // maybe dozens or hundreds more cases
  }
  for _, tt := range tests {
-     t.Run(tt.name, func(t *testing.T) {
+     t.RunName(tt.name, func(t *testing.T) {
          got, err := fizz(tt.input)
          if err != nil {
              t.Fatalf("fizz error: %v", err) // my_test.go:1234
          }
      })
  }
  • We call testing.Name in every test case, which captures file and line information about where the test case was declared.
  • We call testing.T.RunName and pass it the testing.TestName so that the subtest knows what test case is associated with this subtest.

Thus, the test output would be something like:

--- FAIL: Test/Bar (0.00s)
    my_test.go:321: my_test.go:1234: fizz error: the world blew up
  • The my_test.go:321 tells us where the test data was declared.
  • The my_test.go:1234 tells us where in the test logic this failed.

Now, we can click on my_test.go:321 in our code editors and it will take us directly to the test case declaration.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    Accepted

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions