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 koverHtmlReportand 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
// Equality
result shouldBe expected
result shouldNotBe unexpected
// Strings
name shouldStartWith "Al"
name shouldEndWith "ice"
name shouldContain "lic"
name shouldMatch Regex("[A-Z][a-z]+")
name.shouldBeBlank()
// Collections
list shouldContain "item"
list shouldHaveSize 3
list.shouldBeSorted()
list.shouldContainAll("a", "b", "c")
list.shouldBeEmpty()
// Nulls
result.shouldNotBeNull()
result.shouldBeNull()
// Types
result.shouldBeInstanceOf<User>()
// Numbers
count shouldBeGreaterThan 0
price shouldBeInRange 1.0..100.0
// Exceptions
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) // Returns defaults for all calls
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
// build.gradle.kts
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) // Fail build below 80% coverage
}
}
}
}
Coverage Commands
./gradlew koverHtmlReport # Generate HTML report
./gradlew koverVerify # Verify coverage meets thresholds
./gradlew koverXmlReport # Generate XML for CI
# View report (macOS)
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 # Run all tests
./gradlew test --tests "com.example.UserServiceTest" # Run specific class
./gradlew test --tests "com.example.UserServiceTest.getUser*" # Run by pattern
./gradlew test --info # Verbose output
./gradlew koverHtmlReport # Tests + coverage report
./gradlew detekt # Static analysis
./gradlew ktlintCheck # Formatting check
./gradlew test --continuous # Watch mode
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 useevery/verifyfor coroutines. - Use
runTestfor coroutines — Never useThread.sleep()in coroutine tests; useadvanceTimeByoradvanceUntilIdle. - 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 = truefor 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
advanceTimeByorrunTestscheduler 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.mdfor 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.