onsi / ginkgo

A Modern Testing Framework for Go
http://onsi.github.io/ginkgo/
MIT License
8.22k stars 650 forks source link

How to use DescribeTable with SpecContext #1264

Closed plastikfan closed 1 year ago

plastikfan commented 1 year ago

I currently have a set of It based unit tests which look like this:

            It("🧪 should: run with context", func(ctx SpecContext) {
                defer leaktest.Check(GinkgoT())()

                var wg sync.WaitGroup

                path := helpers.Path(root, "RETRO-WAVE")
                optionFn := func(o *nav.TraverseOptions) {
                    o.Store.Subscription = nav.SubscribeFolders
                    o.Store.DoExtend = true
                    o.Callback = asyncCallback("WithCPUPool/primary session")
                    o.Notify.OnBegin = begin("🛡️")
                }
                session := &nav.PrimarySession{
                    Path:     path,
                    OptionFn: optionFn,
                }

                _, err := session.Init().WithCPUPool().Run(&nav.AsyncInfo{
                    Ctx:          ctx,
                    Wg:           &wg,
                    JobsChanOut:  jobsChOut,
                    OutputsChOut: outputsChIn,
                })

                wg.Wait()
                Expect(err).To(BeNil())
                // Eventually(ctx, jobsChOut).WithTimeout(time.Second * 2).Should(BeClosed())
            }, SpecTimeout(time.Second*2))

But since I have a few of test unit tests which look very similar, I want to turn them into a table based test suite. As I started coding up the table, I realised I didnt have a context, which then led me to wonder, well how do I specify a context input param to a DescribeTable test suite.

I did a check with chatgpt, which ended up creating an example that failed at runtime. The table looked something like this:

    DescribeTable("async",
        func(entry *asyncTE) {
            should := fmt.Sprintf("🧪 should: %v", entry.should)
            It(should, func(ctx SpecContext) {
                defer leaktest.Check(GinkgoT())()

                var wg sync.WaitGroup

                path := helpers.Path(root, "RETRO-WAVE")
                optionFn := func(o *nav.TraverseOptions) {
                    o.Store.Subscription = nav.SubscribeFolders
                    o.Store.DoExtend = true
                    o.Callback = asyncCallback("WithCPUPool/primary session")
                    o.Notify.OnBegin = begin("🛡️")
                }
                session := &nav.PrimarySession{
                    Path:     path,
                    OptionFn: optionFn,
                }

                _, err := session.Init().WithCPUPool().Run(&nav.AsyncInfo{
                    Ctx:          ctx,
                    Wg:           &wg,
                    JobsChanOut:  jobsChOut,
                    OutputsChOut: outputsChIn,
                })

                wg.Wait()
                Expect(err).To(BeNil())
            })
        },
        func(entry *asyncTE) string {
            return fmt.Sprintf("===> given: '%v'", entry.given)
        },
        Entry(nil, &asyncTE{
            given:  "WithCPUPool",
            should: "run with context",
        }),
    )

The ginkgo error was:

SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSGinkgo detected an issue with your spec structure
It(should, func(ctx SpecContext) {
/home/plastikfan/dev/github/go/snivilised/extendio/xfs/nav/traverse-navigator-async_test.go:55
  It looks like you are trying to add a [It] node
  to the Ginkgo spec tree in a leaf node after the specs started running.

  To enable randomization and parallelization Ginkgo requires the spec tree
  to be fully constructed up front.  In practice, this means that you can
  only create nodes like [It] at the top-level or within the
  body of a Describe, Context, or When.

  Learn more at:
  http://onsi.github.io/ginkgo/#mental-model-how-ginkgo-traverses-the-spec-hierarchy

FAIL    github.com/snivilised/extendio/xfs/nav  0.011s
ok      github.com/snivilised/extendio/xfs/utils        (cached)
FAIL

So it looks like the It can't be used in this way.

So what is the solution to this, or can't we use DescribeTable for test cases that require a context? (I have looked in the documentation and I can't see any examples that cover this scenario).

onsi commented 1 year ago

hey there - you can use DescribeTable for such usecases but the documentation is a bit buried: https://pkg.go.dev/github.com/onsi/ginkgo/v2#Entry

DescribeTable("async",
    func(ctx SpecContext, entry *asyncTE) {
        defer leaktest.Check(GinkgoT())()
        //use entry in here to configure this particular test

        var wg sync.WaitGroup

        path := helpers.Path(root, "RETRO-WAVE")
        optionFn := func(o *nav.TraverseOptions) {
            o.Store.Subscription = nav.SubscribeFolders
            o.Store.DoExtend = true
            o.Callback = asyncCallback("WithCPUPool/primary session")
            o.Notify.OnBegin = begin("🛡️")
        }
        session := &nav.PrimarySession{
            Path:     path,
            OptionFn: optionFn,
        }

        _, err := session.Init().WithCPUPool().Run(&nav.AsyncInfo{
            Ctx:          ctx,
            Wg:           &wg,
            JobsChanOut:  jobsChOut,
            OutputsChOut: outputsChIn,
        })

        wg.Wait()
        Expect(err).To(BeNil())
    },
    func(entry *asyncTE) string {
        return fmt.Sprintf("%s ===> given: '%v'", entry.should, entry.given)
    },
    Entry(nil, &asyncTE{
        given:  "WithCPUPool",
        should: "run with context",
    }),
)

You don't call It within the body of the table function. It becomes the body of the It. Instead, just as with It, you can have it take a SpecContext as a first argument and Ginkgo will treat it like an It that accepts a SpecContext.

You can then decorate individual entries with SpecTimeout and NodeTimeout if you want to control the timeouts for each entry. Also - while you certainly can create a custom type like asyncTE I tend to just pass arguments in if there are fewer than ~a few:

DescribeTable("async",
    func(ctx SpecContext, given string, should string) {
        defer leaktest.Check(GinkgoT())()
        //use entry in here to configure this particular test

        var wg sync.WaitGroup

        path := helpers.Path(root, "RETRO-WAVE")
        optionFn := func(o *nav.TraverseOptions) {
            o.Store.Subscription = nav.SubscribeFolders
            o.Store.DoExtend = true
            o.Callback = asyncCallback("WithCPUPool/primary session")
            o.Notify.OnBegin = begin("🛡️")
        }
        session := &nav.PrimarySession{
            Path:     path,
            OptionFn: optionFn,
        }

        _, err := session.Init().WithCPUPool().Run(&nav.AsyncInfo{
            Ctx:          ctx,
            Wg:           &wg,
            JobsChanOut:  jobsChOut,
            OutputsChOut: outputsChIn,
        })

        wg.Wait()
        Expect(err).To(BeNil())
    },
    func(given string, should string) string {
        return fmt.Sprintf("%s ===> given: '%v'", should, given)
    },
    Entry(nil, "WithCPUPool", "run with context", SpecTimeout(time.Minute)), //e.g.
)

Finally - I noticed you were using the contents of asyncTE to control the name of the test I've updated the example above to show how to do that since DescribeTable doesn't allow a nested It.

plastikfan commented 1 year ago

Thanks @onsi, your tips worked a treat, just what I was looking for and some nice bonuses, 🙏