Kotest Integrations
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.
- Add jacoco to the plugins in Gradle.
plugins { ... jacoco ... } - Configure jacoco.
jacoco { toolVersion = "0.8.11" reportsDirectory = layout.buildDirectory.dir('customJacocoReportDir') // optional } - Add a jacoco XML report task.
tasks.jacocoTestReport { dependsOn(tasks.test) reports { xml.required.set(true) } } - 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.