question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Nested "before" lifecycle hook

See original GitHub issue

I’m trying to use Kotest to write nested tests that have hooks that only run once per nest level. This is how mocha works in JavaScript and how RSpec works in ruby. In mocha the function is called before. I tried using beforeContainer but it runs once for each nested container instead of just once for the entire container including any nested containers within that parent container. This is hard to explain, so below is a set of tests that I would expect to pass, if a before method existed in kotest that behaved like mocha’s or RSpec’s. In short, each before method should only be called once, and only when tests in its container run (e.g., the before method inside "nested level 2" shouldn’t run until after the tests in "nested level 1" have run).

package foo.bar

import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.shouldBe

class WidgetTest : DescribeSpec({
    var foo = "initial"
    var counterOne = 0
    var counterTwo = 0
    var counterThree = 0

    before {
        foo = "bar"
        counterOne++
    }

    it("foo should be bar") {
        foo shouldBe "bar"
        counterOne shouldBe 1
    }

    it("beforeSpec should have been called only once") {
        foo shouldBe "bar"
        counterOne shouldBe 1
    }

    it("counterTwo should be 0") {
        counterTwo shouldBe 0
    }

    it("counterThree should be 0") {
        counterThree shouldBe 0
    }

    describe("nested level 1") {
        before {
            foo = "buzz"
            counterTwo++
        }

        it("foo should be buzz") {
            foo shouldBe "buzz"
        }

        it("and counterOne should be 1") {
            counterOne shouldBe 1
        }

        it("and counterTwo should be 1") {
            counterTwo shouldBe 1
        }

        describe("nested level 2") {
            before {
                foo = "jazz"
                counterThree++
            }

            it("foo should be jazz") {
                foo shouldBe "jazz"
            }

            it("and counterOne should be 1") {
                counterOne shouldBe 1
            }

            it("and counterTwo should be 1") {
                counterTwo shouldBe 1
            }

            it("and counterThree should be 1") {
                counterThree shouldBe 1
            }
        }
    }
})

I can get close to what I want if I make the first before a beforeSpec and the other two beforeContainer (and move them above each nested container), but then counterTwo gets incremented twice: once for "nested level 1" and once for "nested level 2". I’m not sure if I’m just doing something wrong or if this capability simply doesn’t exist in kotest.

A somewhat more realistic example:

import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import kotlinx.coroutines.delay

class WidgetApiTest : DescribeSpec({
    describe("CRUD operations") {
        var widget: Widget? = Widget(0, "")

        describe("when we create a widget") {
            before { // This should run exactly once
                createWidget(1, "widget-a")
            }

            describe("when we fetch the widget") {
                before { // This should run exactly once
                    widget = getWidget(1) // assume this is a slow HTTP or database request
                }

                it("the widget should not be null") {
                    widget shouldNotBe null
                }

                it("and the widget should have a name") {
                    widget!!.name shouldBe "widget-a"
                }

                describe("when we update the widget's name") {
                    before { // the above "before" functions should not have run again in this test container
                        widget?.let { w ->
                            w.name = "new-name"
                            updateWidget(w)
                        }
                    }

                    it("then the re-fetched widget should be updated") {
                        widget = getWidget(1) // assume this is a slow HTTP or database request
                        widget!!.name shouldBe "new-name"
                    }
                }
            }
        }
    }
})

data class Widget(val id: Int, var name: String?)

val widgetContainer: HashMap<Int, Widget> = hashMapOf()

suspend fun createWidget(id: Int, name: String?): Widget {
    delay(200) // slow HTTP request or Database call
    val widget = Widget(id, name)
    widgetContainer[id] = widget
    return widget
}

suspend fun getWidget(id: Int): Widget? {
    delay(200) // slow HTTP request or Database call
    return widgetContainer[id]
}

suspend fun updateWidget(widget: Widget): Widget {
    delay(200) // slow HTTP request or Database call
    val id = widget.id
    widgetContainer[id] = widget
    return widget
}

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:2
  • Comments:35 (16 by maintainers)

github_iconTop GitHub Comments

2reactions
Reevncommented, Mar 31, 2022

@Kantis I don’t think that restricting the function to the ContainerScope helps to make users understand that it is not inherited to other children ContainerScopes. That’s my personal opinion at least.

I’ll +1 @sksamuel’s suggestion earlier in the thread:

beforeContainer(inherit = false) { }

It already uses the function that is responsible for setting up a container scope, so no new concept or naming introduced, while making it configurable via a parameter to not inherit this callback to children containers. Looks like a pretty good addition to existing API without adding a lot more complexity to the mix (from the public API perspective).

1reaction
BryanDonovancommented, Nov 1, 2022

Any chance this will be added soon? I’m fine with any name really.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Lifecycle events | NestJS - A progressive Node.js framework
Lifecycle events happen during application bootstrapping and shutdown. Nest calls registered lifecycle hook methods on modules , injectables and controllers at ...
Read more >
Kotest: Execution Order of (Nested) Lifecycle Hooks
The problem here is that all extensions are always executed in the order they are registered. This is okay for all before-functions, but...
Read more >
AWS::AutoScaling::LifecycleHook - AWS CloudFormation
A lifecycle hook provides a specified amount of time (one hour by default) to wait for the action to complete before the instance...
Read more >
Vue Parent and Child lifecycle hooks | by Brock Reece - Medium
A component becomes reactive just before the Created hook is fired, this means it will start to track changes of it's props before...
Read more >
Livewire lifecyle hooks for nested collections - Laracasts
on blade before user clicks on dropdown I save the milestone id they are ... than the lifecycle hook runs when the $milestones...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found