ginkgo icon indicating copy to clipboard operation
ginkgo copied to clipboard

QUESTION: using AroundNode to cleanup after tests

Open zhulik opened this issue 3 months ago • 3 comments

Hi, I've just discovered the new AroundNode feature and found it's absolutely awesome, thanks a lot! Yet it may be far more useful.

In according to the docs, an AroundNode when applied to a container, is being executed even for BeforeEach nodes, and this makes it unusable to cleanup after(or before) tests.

For instance, I create some database records in a BeforeEach node and use DeferCleanup to clean the database after:

// root Describe
BeforeEach(func(ctx SpecContext) {
	tenant = factory.CreateTenant(ctx)

	DeferCleanup(func(ctx SpecContext) {
		testhelpers.CleanDB(ctx, db)
	})
})

Since I have a lot of suites working with the database, I don't really want to repeat the same DeferCleanup over and over again and AroundNode would a good place to truncate the database if there was a way to distinguish what node it wraps.

Currently, if I define a decorator like

// Should be used as Describe("SomeDBService", Serial, testhelpers.WithDB(), func...
func WithDB() types.AroundNodeDecorator {
	return ginkgo.AroundNode(func(ctx context.Context, body func(ctx context.Context)) {
		defer CleanDB(ctx, db)
		body(ctx)
	})
}

it will truncate the database after the BeforeEach node, so the test itself will see a clean database. The problem is that not all my tests interact with the database, so I don't need and want to truncate after every single test, that's why a decorator like I described above is useful, I can only apply it to specific containers.

I'm wondering if there is way to know what node an AroundNode wraps so I can only clean the database after actual tests and not after setup nodes.

Maybe there is a different way to achieve what I want?

zhulik avatar Sep 03 '25 16:09 zhulik

To be honest, I'd be much more interested in an around decorator which is executed only once before all BeforeEach nodes and let me execute code after the test finishes, something like:

// Should be used as Describe("SomeDBService", Serial, testhelpers.WithDB(), func...
func WithDB() types.AroundNodeDecorator {
	return ginkgo.AroundTest(func(ctx context.Context, body func(ctx context.Context)) {
            defer CleanDB(ctx, db)
            ctx = context.WithValue(...) // here I also have control over what data is store in the context, all `BeforeEach` nodes will see it
            // Here I can create all necessary records in the database before running the test, there are some I need in all tests
            body(ctx) // Executes all BeforeEach nodes and then the test
	})
}

WDYT?

zhulik avatar Sep 03 '25 16:09 zhulik

hey @zhulik - glad this new features may be useful for you. You've brought up a few separate things - let me dig into them separately.

Cleaning up the database

In cases like what you're describing I tend to do one of two things:

  1. Just add a top-level AfterEach that always calls testhelpers.CleanDB(ctx, db) and not worry about whether a test used the db or not. Often this stuff i plenty fast and the overhead of nuking an empty db is low.

  2. If I want to only clean up tests that actually use the db then I add a testhelpers.SetupDB() that (e.g.) calls factory.CreateTenant and then calls DeferCleanup with testhelpers.CleanDB. That effectively removes the boiler plate and gives me the control I need.

BTW, brief aside: DeferCleanup is smart about functions that take context.Context as their first argument so you can actually do DeferCleanup(testhelpers.CleanDB ,db).

I wouldn't use AroundNode for this sort of thing. Now, if part of the DB setup entails modifying the context and passing that context down to a client things are trickier. Since each node gets its own context you can't piggy back on to the Its context from within a BeforeEach. In these cases you can create a new context.Backgroun() in the BeforeEach, annotate it, and propagate that into the other node closures. If you need to hook that context into Ginkgo's interruption support (e.g. to trigger spec timeouts) you'd need to merge contexts (there are libraries that can do this for you). It's a bit messy, I know, but it's the reality of what exists now.

I'm wondering if there is way to know what node an AroundNode wraps so I can only clean the database after actual tests and not after setup nodes.

There is... it's a bit gnarly and I might make it a first class citizen but I'm not sure this pattern is the best sort of thing to encourage.

But with those caveats aside:

func(ctx context.Context, callback func(ctx.Context)) {
    report := CurrentSpecReport()
    events := report.SpecEvents.WithType(types.SpecEventNodeStart)
    event := events[len(events)-1]
    switch event.NodeType {
        case types.NodeTypeIt:
               callback(ctx)
               //do something after the It node
         default:
                callback(ctx) //just call the node but don't do anything after it
    }
}

like I said, a bit gnarly. But also pretty low-level and so relatively powerful (and easy to abuse!!)

onsi avatar Sep 04 '25 03:09 onsi

Hi @onsi, thanks for such a detailed answer! Using a top-level AfterEach might be an option, but the way my app is structured will probably make it a bit more complicated than described or require some refactoring.

I will try to play with AroundNode and spec events, maybe I'll figure out how to achieve what i need with them.

Thanks!

zhulik avatar Sep 04 '25 10:09 zhulik