QUESTION: using AroundNode to cleanup after tests
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?
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?
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:
-
Just add a top-level
AfterEachthat always callstesthelpers.CleanDB(ctx, db)and not worry about whether a test used thedbor not. Often this stuff i plenty fast and the overhead of nuking an emptydbis low. -
If I want to only clean up tests that actually use the db then I add a
testhelpers.SetupDB()that (e.g.) callsfactory.CreateTenantand then callsDeferCleanupwithtesthelpers.CleanDB. That effectively removes the boiler plate and gives me the control I need.
BTW, brief aside:
DeferCleanupis smart about functions that takecontext.Contextas their first argument so you can actually doDeferCleanup(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!!)
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!