Skip to content

proposal: testing: helpers for named table driven tests #77022

@seankhliao

Description

@seankhliao

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)
                }
        })
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    LibraryProposalIssues describing a requested change to the Go standard library or x/ libraries, but not to a toolProposal

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions