Search code examples
kotlinkotest

Kotest test factory and beforeSpec


Update While searching for the optimal solution, I switched temporarily to an approach using a dedicated test factory that performs the init in combination with forAll. In this way, I don't have to repeat the init code in every test factory.

fun somePreStuffFactory() = funSpec {
    beforeTest {
        println("Do some init stuff here")
    }

    test("dummy test just to trigger beforeTest") {}
}

class MyTest : FunSpec({
    runBlocking {
        forAll(
            row(MyTestFactory::someTest1),
            row(MyTestFactory::someTest2)
        ) { testFactoryFunc ->
            include(somePreStuffFactory())
            include(testFactoryFunc())
        }
    }
})

End of update

I have a lot of tests inside test factories. Before running the tests inside a test factory, I need to do some initial setup in the beginning of each test factory, and the setup is the same for all test factories.

Problem is that beforeSpec is not invoked inside test factory, hence I'm currently using a dirty workaround by doing init stuff in the first test in each test factory. I would highly appreciate any advice on this.

The optimal solution would be to have a life-cycle hook inside the the test class that runs before each test factory.

Code to reproduce

class MyTest : FunSpec({
    include(someTest1())
    include(someTest2())
})
import io.kotest.core.spec.style.funSpec

object MyTestFactory {
    fun someTest1() = funSpec {
        beforeSpec {
            /** Not invoked */
            println("Hello from someTest1#beforeSpec")
        }

        test("Init stuff is done inside a test") {
            /** some init here */
        }

        test("first test") {
            println("Hello from first test")
        }

        test("second test") {
            println("Hello from second test")
        }
    }

    fun someTest2() = funSpec {
        beforeSpec {
            /** Not invoked */
            println("Hello from someTest2#beforeSpec")
        }

        test("Init stuff is done inside a test") {
            /** some init here */
        }

        test("third test") {
            println("Hello from third test")
        }
    }
}

What I've tried so far

After bumping Kotest from 4.6.4 to 5.4.2, I was able to run the code in answer from @ocos. Problem is that BeforeSpecSample#beforeSpec is invoked just once, not for each test factory which is my requirement.

object BeforeSpecSample : BeforeSpecListener {
    override suspend fun beforeSpec(spec: Spec) {
        println("Hello from beforeSpec")
    }
}

class MyTest : FunSpec({
    extensions(BeforeSpecSample)

    include(someTest1())
    include(someTest2())
})

Update After reading this GitHub issue, I successfully tested the following approach using a boolean var initialized andbeforeTest. It would have been nice if this approach could be used inside test class instead of inside each test factory, but without any life-cycle hooks for test factories, I don't see how that can be done.

fun someTest1() = funSpec {
    var initialized = false

    beforeTest {
        if (!initialized) {
            println("Hello from someTest1#beforeTest")
            initialized = true
        }
    }

    /** tests goes here */
}

Environment Kotest 4.6.4, Kotlin 1.7.10, Micronaut 3.6.3

Kotest test factory doc


Solution

  • If you don't mind grouping everything inside a test factory in a context block, you could use a BeforeContainerListener for initialization:

    object InitExtension : BeforeContainerListener {
        override suspend fun beforeContainer(testCase: TestCase) {
            if(testCase.parent == null) {
                println("init stuff")
            }
        }
    }
    

    The if(testCase.parent == null) is just there to allow your factory to have other nested contexts that will not trigger an additional invocation of the initialization.

    Then you can write your factory like this:

    fun someTest1() = funSpec {
        extension(InitExtension)
        context("someTest1") {
            test("first test") {
                println("Hello from first test")
            }
    
            test("second test") {
                println("Hello from second test")
            }
        }
    }
    

    The initialization in InitExtension will be called at the beginning of context someTest1, and can analogously be included in other test factories that can each be initialized by the same extension.