Description
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 thetesting.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
Type
Projects
Status
Activity
randall77 commentedon May 7, 2022
This is similar to the workaround we use in the stdlib, e.g.,
reflect.verifyGCBits
.randall77 commentedon May 7, 2022
That said, it would be nice to have something ergonomic built in to
testing
. I wishtesting.Run
took anany
instead of astring
as the name, so we didn't have to introduce another method onTesting
to enable this.dsnet commentedon May 9, 2022
To avoid declaring new
RunName
methods, we could do something like:where
testing.NameFileLine.String
prints in a special syntax recognized bytesting.T.Run
andtesting.B.Run
. However, this might be considered a breaking change.rsc commentedon May 11, 2022
It seems like there are two things going on here:
It seems like we could keep t.Run the same, pass tt.name.String() to it, and then separately have a
call at the start of the function body. Then other sources of file:line (like testscript failures) can hook into this too.
rsc commentedon May 11, 2022
This proposal has been added to the active column of the proposals project
and will now be reviewed at the weekly proposal review meetings.
— rsc for the proposal review group
dnephin commentedon May 12, 2022
I have encountered this problem a few times. I'll share the workaround I came up with in case it helps this proposal in some way.
Using
t.Helper
and arun
function I was able to have the tests emit at least 2 file and line number outputs: one for the first line of the test case definition, and one from the test logic function where the failure happened.To make this work, the traditional test case "table" has to be modified a bit.
run
function, instead of at the end.run
. It's a few extra characters per test case.Using the example test from the description, it might look like this:
Definitely not as elegant as the
fileLine
workaround.t.AddFileLine
would be a great addition!rsc commentedon May 25, 2022
I'd still like to understand better whether we can separate out the two different features being added, as I noted in #52751 (comment). Any thoughts, @dsnet?
AlexanderYastrebov commentedon May 27, 2022
Since new type is introduced maybe there is no need to have
String
andAddFileLine
methods but just aRun
wrapper:dsnet commentedon May 27, 2022
@AlexanderYastrebov, you would need two different
Run
methods since the method must taking in either afunc(*testing.T)
or afunc(*testing.B)
. It's not clear that it's a cleaner API than what was originally proposed.AlexanderYastrebov commentedon May 28, 2022
@dsnet
rsc commentedon Jun 1, 2022
@dsnet, any thoughts on #52751 (comment) ?
dsnet commentedon Jun 8, 2022
In regards to #52751 (comment), it seems odd if the test name is annotated with file:line information if we can directly use the test name and file:line information together. There's a discrepancy between how creation and usage operates.
If usage is segregated, then creation should also be segregated:
This approach is more flexible as you can imagine using this for more purposes than just annotating test case names.
However, it is a little more typing than my original proposal in #52751 (comment).
I would be okay with something like this API.
In response to #52751 (comment), one significant detriment to
func (t *T) Case(name string) string
is that it assumes that we have atesting.T
on hand when crafting the test cases. I have some tests that share a global testcases table. There is no singletesting.T
around at the time that the table was constructed.rsc commentedon Jun 8, 2022
@dsnet, you jumped to testing.SourceLocation, which I didn't really have in mind. I think testing.Name returning something that has a name and a file:line is fine. But there are other tests reading test data files that might also want to set the file:line, and I was suggesting that they could use that mechanism too if it were split out. I don't think we need a SourceLocation type though.
So only the t.Annotate would be added in the diff, not all the testing.FileLine() calls.
78 remaining items