| name | kotlin-testing |
| description | Kotlin testing patterns with Kotest, MockK, coroutine testing, property-based testing, and Kover coverage. Follows TDD methodology with idiomatic Kotlin practices. |
Kotlin Testing Patterns
Comprehensive Kotlin testing patterns for writing reliable, maintainable tests following TDD methodology with Kotest and MockK.
When to Use
- Writing new Kotlin functions or classes
- Adding test coverage to existing Kotlin code
- Implementing property-based or data-driven tests
- Following TDD workflow in Kotlin projects
- Configuring Kover for code coverage
How It Works
- Identify target code — Find the function, class, or module to test
- Write a Kotest spec — Choose a spec style (FunSpec, StringSpec, BehaviorSpec, DescribeSpec) matching the test scope
- Mock dependencies — Use MockK to isolate the unit under test
- Run tests (RED) — Verify the test fails with the expected error
- Implement code (GREEN) — Write minimal code to pass the test
- Refactor — Improve the implementation while keeping tests green
- Check coverage — Run
./gradlew koverHtmlReport and verify 80%+ coverage
TDD Cycle
RED → Write a failing test first
GREEN → Write minimal code to pass the test
REFACTOR → Improve code while keeping tests green
REPEAT → Continue with next requirement
For full step-by-step EmailValidator walkthrough, see references/code-examples.md.
Kotest Spec Styles
FunSpec (Recommended for Most Tests)
class UserServiceTest : FunSpec({
val repository = mockk<UserRepository>()
val service = UserService(repository)
test("getUser returns user when found") {
val expected = User(id = "1", name = "Alice")
coEvery { repository.findById("1") } returns expected
val result = service.getUser("1")
result shouldBe expected
}
test("getUser throws when not found") {
coEvery { repository.findById("999") } returns null
shouldThrow<UserNotFoundException> {
service.getUser("999")
}
}
})
For StringSpec, BehaviorSpec, and DescribeSpec examples, see references/code-examples.md.
Kotest Matchers
Core Matchers
result shouldBe expected
result shouldNotBe unexpected
name shouldStartWith "Al"
name shouldEndWith "ice"
name shouldContain "lic"
name shouldMatch Regex("[A-Z][a-z]+")
name.shouldBeBlank()
list shouldContain "item"
list shouldHaveSize 3
list.shouldBeSorted()
list.shouldContainAll("a", "b", "c")
list.shouldBeEmpty()
result.shouldNotBeNull()
result.shouldBeNull()
result.shouldBeInstanceOf<User>()
count shouldBeGreaterThan 0
price shouldBeInRange 1.0..100.0
shouldThrow<IllegalArgumentException> {
validateAge(-1)
}.message shouldBe "Age must be positive"
shouldNotThrow<Exception> {
validateAge(25)
}
For custom matchers, see references/code-examples.md.
MockK Basics
class UserServiceTest : FunSpec({
val repository = mockk<UserRepository>()
val logger = mockk<Logger>(relaxed = true)
val service = UserService(repository, logger)
beforeTest {
clearMocks(repository, logger)
}
test("findUser delegates to repository") {
val expected = User(id = "1", name = "Alice")
every { repository.findById("1") } returns expected
val result = service.findUser("1")
result shouldBe expected
verify(exactly = 1) { repository.findById("1") }
}
test("suspend function mocking") {
coEvery { repository.findById("1") } returns User(id = "1", name = "Alice")
val result = service.getUser("1")
result.name shouldBe "Alice"
coVerify { repository.findById("1") }
}
})
For advanced MockK patterns (argument capture, spy, coroutine mocking), see references/code-examples.md.
Coroutine Testing
Use runTest from kotlinx.coroutines.test for suspend functions and coroutines:
test("concurrent fetches complete together") {
runTest {
val service = DataService(testScope = this)
val result = service.fetchAllData()
result.users.shouldNotBeEmpty()
}
}
For Flow testing, TestDispatcher, and advanced coroutine patterns, see references/code-examples.md.
Property-Based & Data-Driven Testing
Property-based testing: Automatically generates test cases using Kotest's Arb (arbitrary) generators. Great for pure functions.
Data-driven testing: withData allows parameterized test cases for multiple inputs.
Examples and custom generators available in references/code-examples.md.
Test Lifecycle and Fixtures
Use beforeTest/afterTest for test-level setup/cleanup, and beforeSpec/afterSpec for suite-level initialization.
beforeSpec { db = setupDatabase() }
afterSpec { db.close() }
beforeTest { db.clear() }
Kotest extensions enable reusable test infrastructure. See references/code-examples.md for DatabaseExtension example.
Kover Coverage Configuration
Gradle Setup
plugins {
id("org.jetbrains.kotlinx.kover") version "0.9.7"
}
kover {
reports {
total {
html { onCheck = true }
xml { onCheck = true }
}
filters {
excludes {
classes("*.generated.*", "*.config.*")
}
}
verify {
rule {
minBound(80)
}
}
}
}
Coverage Commands
./gradlew koverHtmlReport
./gradlew koverVerify
./gradlew koverXmlReport
open build/reports/kover/html/index.html
Coverage Targets
| Code Type | Target |
|---|
| Critical business logic | 100% |
| Public APIs | 90%+ |
| General code | 80%+ |
| Generated / config code | Exclude |
Testing Commands
./gradlew test
./gradlew test --tests "com.example.UserServiceTest"
./gradlew test --tests "com.example.UserServiceTest.getUser*"
./gradlew test --info
./gradlew koverHtmlReport
./gradlew detekt
./gradlew ktlintCheck
./gradlew test --continuous
Effective Patterns
- Write tests FIRST — Follow TDD strictly. RED phase catches bugs early.
- Use Kotest consistently — Pick a spec style (FunSpec recommended) and stick with it across the project.
- Mock suspend functions with
coEvery/coVerify — Never use every/verify for coroutines.
- Use
runTest for coroutines — Never use Thread.sleep() in coroutine tests; use advanceTimeBy or advanceUntilIdle.
- Test behavior, not implementation — Focus on what the code does, not how it does it.
- Use real instances for immutable objects — Don't mock data classes; create real test fixtures.
- Leverage property-based testing — Excellent for pure functions and edge case discovery.
- Keep fixtures in
data classes — Makes test intent clear and setup reusable.
- Use
relaxed = true for loggers/observers — They're usually not critical to test assertions.
Patterns to Avoid
- Mixing test frameworks — Kotest and JUnit don't play well together; commit fully to Kotest.
- Mocking immutable objects — Use real instances instead for simplicity.
- Complex test setup — If setup takes >10 lines, extract to a helper or factory.
- Flaky tests from timing — Use
advanceTimeBy or runTest scheduler control instead of delays.
- Testing private functions directly — Private functions are tested indirectly via public APIs.
- Ignoring test failures — Flaky tests hide real bugs; fix or skip immediately.
- Tight coupling to implementation — Refactorings should not require rewriting tests.
Quick References
- Kotest spec styles: See
references/code-examples.md for StringSpec, BehaviorSpec, DescribeSpec
- EmailValidator TDD walkthrough: Full RED-GREEN-REFACTOR example in
references/code-examples.md
- Advanced MockK: Argument capture, spy, coroutine mocking in
references/code-examples.md
- Flow testing: Testing Kotlin Flows and debounce patterns in
references/code-examples.md
- Custom matchers: Build domain-specific assertions in
references/code-examples.md
- CI/CD integration: GitHub Actions YAML workflow in
references/code-examples.md
Remember: Tests are documentation. They show how your Kotlin code is meant to be used. Use Kotest's expressive matchers to make tests readable and MockK for clean mocking of dependencies.