ginkgo icon indicating copy to clipboard operation
ginkgo copied to clipboard

Question: Shared or Reusable Context

Open rejoshed opened this issue 2 years ago • 4 comments

Hey, thanks for the awesome test lib!

I was trying to write a context block that I can reuse.

Essentially I want something that I can use like:

CoolContext = Context("So cool!", func() {
    BeforeEach(func(){
        ... party ...
    })
    AfterEach(func(){
        ... cleanup ...
        ... nurse hangover ...
    })
})
In(CoolContext(
    It("rocks", func(){
        Ω(... always wears sunglasses at night ...)
    })
))

Is there an existing pattern that would allow this?

rejoshed avatar May 20 '22 22:05 rejoshed

Hi @rejoshed. You can just use higher order functions in Go (that is functions that return functions). For example:

func contextFuncGenerator(s string) func() {
  return func() {
    BeforeEach(func() { ... })
    AfterEach(func() { ... })
    It(s, func() { ... })
  }
}

Context("one", contextFuncGenerator("alpha"))
Context("two", contextFuncGenerator("beta"))

One gotcha involves when functions are actually invoked: Describe/Context/When immediately invoke their callback function, while It/Spec callback functions are invoked later when the test is actually run. This causes problems in for loops, for example:

for i := range []int{1,2,3} {
  It(fmt.Sprintf("matches %d", i), func() {
    Expect(answer()).To(Equal(i)) // because the It() is invoked after the loop finishes, 'i' is always 3
  })
}

It's worth referring to the docs for more details. My advice would be to do this sparingly as it's easy to get wrong, and can be tricky to understand.

In the case you talk about above, you could just have another It() in the same Context(). I'm assuming you know that, and your actual use case is more complex.

blgm avatar May 21 '22 16:05 blgm

+1. Here's a couple of additional ideas/patterns. There really isn't a "right" or "wrong" way to do any of this - just whatever makes sense to you and helps you manage complexity:

You can write CoolContext as:

func CoolContext(name string, specs func()) {
    Context(fmt.Sprintf("So cool - %s", name), Offset(1), func() {
        BeforeEach(func(){
            ... party ...
        })
        AfterEach(func(){
            ... cleanup ...
            ... nurse hangover ...
        })

        specs()    
    }
}

and then your tests look like:

CoolContext("bob", func() {
    It("rocks", func(){
        Ω(... always wears sunglasses at night ...)
    })

    It("rocks again", func(){
        Ω(... always wears sunglasses at night ...)
    })
})

Though in this case of shared setup/teardown I tend to prefer pulling out functions that do the cleanup/teardown (using Ginko's DeferCleanup):


func PrepForParty() {
    ... party ...
    DeferCleanup( ...cleanup... )
    DeferCleanup( ...nurst hangover... )
}

and now you can just:

Context("So cool bob!", func() {
    BeforeEach(func(){
        PrepForParty()
    })

    It("rocks", func(){
        Ω(... always wears sunglasses at night ...)
    })

    It("rocks again", func(){
        Ω(... always wears sunglasses at night ...)
    })
})

onsi avatar May 21 '22 17:05 onsi

Wow! Thanks for all the great answers. I'll give them a try sometime in the next day or two.

I did try the higher order function option, but I had troubles when running a single test using my IDE. I'm not sure exactly which command it runs to run only the single test, so I'll need to check into that.

Basically when I ran the outer context, it worked fine, but when I ran the single test, it lost (I forgot at this point) either the outer or inner context.

I'm running ginkgo V2 atm btw.

Thanks again, and I'll update once I've tried some more things.

rejoshed avatar May 23 '22 16:05 rejoshed

The IDE could struggle in that situation. You could mark the test as focused (either with FIt or the Focus decorator) and try to run all the tests, allowing Ginkgo to work out which test to run.

blgm avatar May 23 '22 21:05 blgm