1. Introduction

Unit testing is a crucial aspect of software development, ensuring the reliability and correctness of our code. When it comes to testing Kotlin applications, we often rely on powerful testing frameworks like Kotest, which offer a wide range of features. Three important features provided by Kotest are beforeSpec(), beforeEach(), and beforeInvocation(). With these features, we can run functions before the entire test suite, before every test, and before each invocation, respectively.

In this article, we’ll explore the step-by-step process of implementing the beforeSpec(), beforeEach(), and beforeInvocation() lifecycle hooks in Kotest.

2. Before All Tests in a Spec

In Kotest, the beforeSpec() function allows us to run a function that executes one time only, before the first test within a specific test suite. This functionality is useful for preparing a consistent environment for all tests in the suite and reducing redundant setup tasks.

To use beforeSpec() at the test suite level, we define it within a Spec:

class BeforeSpecSamples : FunSpec({

    val userRepository = UserRepository()

    beforeSpec {
        userRepository.addUser(User(1, "Admin"))
    }

   // Our test cases
})

The beforeSpec() block contains the setup code that will be executed once, before the first test case within the test suite. It’s important to note that the setup code will be shared among all the tests within the suite.

2.1. Isolation Mode and beforeSpec()

Kotest has three IsolationModes: SingleInstance (default), InstancePerLeaf, and InstancePerTest. It’s important to note that if we use any of the non-default modes, beforeSpec() might be invoked multiple times. Therefore, when selecting the IsolationMode, caution is necessary to avoid or handle any undesired executions effectively. We’ll discuss isolation modes in depth below.

3. Before Each Test

Before running individual test cases, we can utilize the beforeEach() function to perform setup tasks pertaining to each test in a test class. Let’s explore the purpose and syntax of beforeEach(), understanding how it enhances the independence and reusability of our tests. By effectively using this feature, we can prepare our test environment before executing each test within our spec.

In Kotest, we invoke the beforeEach() function within a test spec or test class:

class BeforeTestSamples : FunSpec({

    val userRepository = UserRepository()

    beforeEach {
        userRepository.addUser(User(1, "Admin"))
    }

    test("Accessing all users should include added user and Admin") {
        userRepository.addUser(User(2, "SimpleUser"))

        userRepository.all() shouldBe listOf(
            User(1, "Admin"),
            User(2, "SimpleUser")
        )
    }
})

In the above example, the beforeEach() block defines the setup code that will be executed before each test case within the test spec. We can include any necessary setup steps, such as initializing objects, configuring dependencies, or setting up mock data. Our test case validates that the user, Admin, added in beforeEach(), is present when we query for all users.

4. Before Test Invocation

Kotest can run a test more than once by setting the number of invocations. If we need to run some setup before each individual invocation, we’ll need the beforeInvocation() lifecycle hook. This hook can be leveraged by providing an implementation of BeforeInvocationListener:

class BeforeInvocationSamples : FunSpec({

    val userRepository = UserRepository()

    isolationMode = IsolationMode.InstancePerTest

    extension(object : BeforeInvocationListener {
        override suspend fun beforeInvocation(testCase: TestCase, iteration: Int) {
            userRepository.addUser(User(iteration.toLong(), "Admin"))
        }
    })


    test("Accessing all users should include added user and Admin").config(invocations = 30) {
        userRepository.addUser(User(2, "SimpleUser"))

        userRepository.all().last() shouldBe User(2, "SimpleUser")
    }
})

In this code snippet, we’ve inserted an Admin for each of our invocations. With BeforeInvocationListener, we can fine-tune how our setup code is run.

5. Isolation Mode

Kotest offers an isolation mode that further enhances the control and predictability of our tests. By default, Kotest uses the InstancePerSpec isolation mode, ensuring that each test spec runs as its own instance.

In the InstancePerSpec isolation mode, all tests within a spec share the same instance. This means that any setup performed in the beforeEach() block will persist across all tests within the spec. While this can provide a slight performance improvement by reusing the same instance, it’s important to note that tests within a spec should still be independent and not rely on the state of other tests.

We can override the isolation mode to customize this behavior by setting isolationMode in our test spec or suite:

class BeforeTestSamples : FunSpec({

    val userRepository = UserRepository()

    isolationMode = IsolationMode.InstancePerTest

    beforeEach {
        userRepository.addUser(User(1, "Admin"))
    }

    test("Accessing all users should include added user and Admin") {
        userRepository.addUser(User(2, "SimpleUser"))

        userRepository.all() shouldBe listOf(
            User(1, "Admin"),
            User(2, "SimpleUser")
        )
    }
})

In the above example, the isolationMode is overridden to set the isolation mode to IsolationMode.InstancePerTest. This means that each test will run in its own instance, ensuring a clean and independent state for each test.

Configuring the isolation mode allows us to have more control over the test environment and avoid interference between tests. It can be particularly useful when tests have dependencies or when setup and teardown operations are expensive or have side effects.

However, it’s important to note that changing the isolation mode may impact test performance, as each test will incur the overhead of creating a new instance. Therefore, it’s crucial to consider the trade-off between test independence and test execution time.

6. Conclusion

In this article, we explored the powerful beforeSpec(), beforeEach(), and beforeInvocation() hooks in Kotest, which allow us to set up our tests effectively and consistently. By leveraging these features, we can ensure proper test environment preparation before each test case, at the beginning of the test suite, and at the beginning of each invocation.

By incorporating these functionalities into our testing workflow, we improve the reliability, maintainability, and efficiency of our unit tests. They establish consistent and controlled testing environments, reduce code duplication, and ensure test case independence.

As always, the code used in this article is available over on GitHub.