Kotest Integrations

You can write more robust, higher-quality tests by using Kotest together with the mocking framework mockk and JaCoCo. This page explains mockk, which helps with test isolation and dependency management, and JaCoCo, a code coverage tool that measures how much of a codebase is tested.

Mocking and Kotest

Kotest itself does not include mocking functionality. However, you can easily plug in your preferred mocking library.

For example, let’s look at mockk:

class MyTest : FunSpec({

    val repository = mockk<MyRepository>()
    val target = MyService(repository)

    test("Saves to repository") {
        every { repository.save(any()) } just Runs
        target.save(MyDataClass("a"))
        verify(exactly = 1) { repository.save(MyDataClass("a")) }
    }
})

This example works as expected, but what happens if you add more tests that use the same mockk mock?

class MyTest : FunSpec({

    val repository = mockk<MyRepository>()
    val target = MyService(repository)

    test("Saves to repository") {
        every { repository.save(any()) } just Runs
        target.save(MyDataClass("a"))
        verify(exactly = 1) { repository.save(MyDataClass("a")) }
    }

    test("Saves to repository as well") {
        every { repository.save(any()) } just Runs
        target.save(MyDataClass("a"))
        verify(exactly = 1) { repository.save(MyDataClass("a")) }
    }

})

The snippet above throws an exception.

2 matching calls found, but needs at least 1 and at most 1 calls

This happens because the mock is not reset between calls. By default, Kotest isolates tests by creating a single instance of the spec for all tests that will run.

Because of this, the mock is reused. How can we solve this?

Option 1 - Set Up Mocks Before Each Test

class MyTest : FunSpec({

    lateinit var repository: MyRepository
    lateinit var target: MyService

    beforeTest {
        repository = mockk()
        target = MyService(repository)
    }

    test("Saves to repository") {
        // ...
    }

    test("Saves to repository as well") {
        // ...
    }

})

Option 2 - Reset Mocks After Each Test

class MyTest : FunSpec({

    val repository = mockk<MyRepository>()
    val target = MyService(repository)

    afterTest {
        clearMocks(repository)
    }

    test("Saves to repository") {
        // ...
    }

    test("Saves to repository as well") {
        // ...
    }

})

Placing Listeners

Any function executed inside a spec definition can place listeners at the end.

class MyTest : FunSpec({

    val repository = mockk<MyRepository>()
    val target = MyService(repository)

    test("Saves to repository") {
        // ...
    }

    test("Saves to repository as well") {
        // ...
    }

    afterTest {
        clearMocks(repository)  // <---- End of file, better readability
    }

})

Option 3 - Adjust the Isolation Mode

Depending on your use case, adjusting the isolation mode for a specific spec may also be a good approach. To learn more about isolation modes, see the Isolation Modes document.

class MyTest : FunSpec({

    val repository = mockk<MyRepository>()
    val target = MyService(repository)


    test("Saves to repository") {
        // ...
    }

    test("Saves to repository as well") {
        // ...
    }

    isolationMode = IsolationMode.InstancePerTest

})

JaCoCo

Kotest integrates with JaCoCo for code coverage in the standard Gradle way. You can find the Gradle installation instructions here.

  1. Add jacoco to the plugins in Gradle.
    plugins {
        ...
        jacoco
        ...
    }
    
  2. Configure jacoco.
    jacoco {
        toolVersion = "0.8.11"
        reportsDirectory = layout.buildDirectory.dir('customJacocoReportDir') // optional
    }
    
  3. Add a jacoco XML report task.
    tasks.jacocoTestReport {
        dependsOn(tasks.test)
        reports {
            xml.required.set(true)
        }
    }
    
  4. Change the test task so it is finalized by jacoco.
    tasks.test {
        ...
        finalizedBy(tasks.jacocoTestReport)
    }
    

Now, when you run test, the JaCoCo report file is generated under $buildDir/reports/jacoco.

Note: In a multi-module project, you may need to apply the jacoco plugin to each submodule.


References