-
Notifications
You must be signed in to change notification settings - Fork 18.7k
Description
Proposal Details
helpers for named table driven tests
API additions
package testing
// Corpus holds a set of test data for use in tests, benchmarks, or fuzz tests.
type Corpus[D any] struct{
// unexported fields
}
// Add a test case to the corpus.
func (c *Corpus[D]) Add(name string, data D) {}
// Test runs the given test function against all entries in the corpus
func (c *Corpus[D]) Test(t *T, f func(t *T, data D))
// Benchmark runs the given benchmark function against all entries in the corpus
func (c *Corpus[D]) Benchmark(b *B, f func(b *B, data D))
// Seed adds all the entries in the corpus as named seeds to the given fuzz test
func (c *Corupus[D]) Seed(f *F)output format
=== RUN TestFoo
=== ATTR TestFoo CorpusEntry my_test.go:1234
--- FAIL: TestFoo (0.00s)
why
This is an alternative to #52751 and #70070
The accepted proposal for #52751 is quite verbose,
requiring a Pos field in every test case struct,
and a Mark entry in every definition.
The variations in the comments are fairly similar,
mostly centering around making name hold more data.
I think we should instead work around that by wrapping the entire table definition instead.
Corpus would be equivalent to the ad-hoc slices defined today to hold test cases,
and by running the test function from the corpus,
one level of indenting can be eliminated in the common case of using a loop and a subtest.
Since Corpus would hold named test entries,
I think it can be reused for seeding the fuzz tests with names.
For the output, I would rather not have the double my_test.go:1234 my_test.go:4321 ... format which becomes unwieldy especially with long file names or full paths.
I think an attribute logged once at the start of the test is the right way to represent this.
Example: baseline
This is a baseline test function
func TestTrimFoo_orig(t *testing.T) {
entries := []struct {
name string
in string
want string
}{{
name: "empty",
in: "",
want: "",
}, {
name: "foo",
in: "foo",
want: "",
}, {
name: "bar",
in: "bar",
want: "bar",
}}
for _, c := range cases {
t.Run(e.name, func(t *testing.T) {
got := strings.TrimPrefix(e.in, "foo")
if got != e.want {
t.Error("f(%q) = %q, want = %q", e.in, got, e.want)
}
})
}
}Example: inline table in test function
This is the test function above, translated to the new api.
It is denser than the original (name and a brace are combine on one line),
but it does involve creating a named type to hold the test case.
The actual test function is also one level less indented.
func TestTrimFoo_inline(t *testing.T) {
type entry struct {
in string
want string
}
var corpus testing.Corpus[entry]
corpus.Add("empty", entry{
in: "",
want: "",
})
corpus.Add("foo", entry{
in: "foo",
want: "",
})
corpus.Add("bar", entry{
in: "bar",
want: "bar",
})
corpus.Test(t, func(t *testing.T, e entry) {
got := strings.TrimPrefix(e.in, "foo")
if got != e.want {
t.Error("f(%q) = %q, want = %q", e.in, got, e.want)
}
})
}Example: shared corpus for different tests
This is shows the same corpus definition being reused for different tests/fuzz tests.
type trimFooEntry struct {
in string
want string
}
var trimFooCorpus = func() *testing.Corpus[trimFooEntry] {
var corpus testing.Corpus[trimFooEntry]
corpus.Add("empty", trimFooEntry{
in: "",
want: "",
})
corpus.Add("foo", trimFooEntry{
in: "foo",
want: "",
})
corpus.Add("bar", trimFooEntry{
in: "bar",
want: "bar",
})
return corpus
}
func TestTrimFoo_shared(t *testing.T) {
corpus.Test(t, func(t *testing.T, e entry) {
got := strings.TrimPrefix(e.in, "foo")
if got != e.want {
t.Error("f(%q) = %q, want = %q", e.in, got, e.want)
}
})
}
func FuzzTrimFoo_shared(f *testing.F) {
corpus.Seed(f)
f.Fuzz(func(e entry){
got := strings.TrimPrefix(e.in, "foo")
if got != e.want {
t.Error("f(%q) = %q, want = %q", e.in, got, e.want)
}
})
}