Nested "before" lifecycle hook
See original GitHub issueI’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:
- Created 2 years ago
- Reactions:2
- Comments:35 (16 by maintainers)
Top GitHub Comments
@Kantis I don’t think that restricting the function to the
ContainerScope
helps to make users understand that it is not inherited to other childrenContainerScope
s. That’s my personal opinion at least.I’ll +1 @sksamuel’s suggestion earlier in the thread:
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).
Any chance this will be added soon? I’m fine with any name really.