| name | kotlin-testing |
| description | Kotlin testing with Kotest, MockK, coroutine testing, property-based testing, and Kover coverage. Use when writing, reviewing, or debugging Kotlin tests or setting up test infrastructure. |
| origin | MCC |
Kotlin Testing Patterns
Idiomatic Kotlin testing using Kotest and MockK following TDD methodology. Tests are documentation — they show how code is meant to be used.
When to Use
- Writing new Kotlin functions or classes
- Adding test coverage to existing Kotlin code
- Implementing property-based tests
- Following TDD workflow in Kotlin projects
- Configuring Kover for code coverage
- Writing Ktor integration tests
TDD Workflow
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
fun validateEmail(email: String): Result<String> {
TODO("not implemented")
}
class EmailValidatorTest : StringSpec({
"valid email returns success" {
validateEmail("user@example.com").shouldBeSuccess("user@example.com")
}
"empty email returns failure" {
validateEmail("").shouldBeFailure()
}
})
fun validateEmail(email: String): Result<String> {
if (email.isBlank()) return Result.failure(IllegalArgumentException("Email cannot be blank"))
if ('@' !in email) return Result.failure(IllegalArgumentException("Email must contain @"))
val regex = Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
if (!regex.matches(email)) return Result.failure(IllegalArgumentException("Invalid email format"))
return Result.success(email)
}
Kotest Spec Styles
Choose one style per project and stick with it:
| Style | Best For | Syntax |
|---|
| StringSpec | Simple unit tests | "test name" { ... } |
| FunSpec | JUnit-like familiar style | test("name") { ... } |
| BehaviorSpec | BDD, complex scenarios | Given/When/Then |
| DescribeSpec | RSpec-style nested contexts | describe/context/it |
See kotest-examples.md for full examples of each style.
MockK Essentials
val repository = mockk<UserRepository>()
val logger = mockk<Logger>(relaxed = true)
every { repository.findById("1") } returns expected
verify(exactly = 1) { repository.findById("1") }
coEvery { repository.findById("1") } returns expected
coVerify { repository.findById("1") }
val slot = slot<User>()
coEvery { repository.save(capture(slot)) } returns Unit
slot.captured.name shouldBe "Alice"
See mockk-examples.md for mocking, spying, and argument capture patterns.
Coroutine Testing
Always use runTest from kotlinx.coroutines.test:
test("concurrent fetches complete together") {
runTest {
val result = service.fetchAllData()
result.users.shouldNotBeEmpty()
}
}
test("timeout after delay") {
runTest {
shouldThrow<TimeoutCancellationException> {
withTimeout(100) { service.slowOperation() }
}
}
}
See coroutine-testing.md for Flow testing, TestDispatcher, and advanceTimeBy patterns.
Core Matchers
result shouldBe expected
name shouldStartWith "Al"
name shouldContain "lic"
list shouldContain "item"
list shouldHaveSize 3
list.shouldBeSorted()
result.shouldNotBeNull()
shouldThrow<IllegalArgumentException> {
validateAge(-1)
}.message shouldBe "Age must be positive"
Property-Based Testing
test("string reverse is involutory") {
forAll<String> { s -> s.reversed().reversed() == s }
}
test("list sort is idempotent") {
forAll(Arb.list(Arb.int())) { list ->
list.sorted() == list.sorted().sorted()
}
}
See advanced-testing.md for property-based testing, data-driven testing, and custom generators.
Kover Coverage
plugins {
id("org.jetbrains.kotlinx.kover") version "0.9.7"
}
kover {
reports {
verify { rule { minBound(80) } }
}
}
./gradlew koverHtmlReport
./gradlew koverVerify
| 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 --info
./gradlew koverHtmlReport
./gradlew detekt
./gradlew test --continuous
Best Practices
DO:
- Write tests FIRST (TDD) — tests are executable specifications
- Use Kotest's spec styles consistently across the project
- Use MockK's
coEvery/coVerify for suspend functions because they handle coroutine context properly
- Use
runTest for coroutine testing because it controls virtual time
- Test behavior, not implementation — test what a function returns, not how it computes it
- Use property-based testing for pure functions to catch edge cases you would not think of
DON'T:
- Mix testing frameworks — pick Kotest and stick with it for consistency
- Mock data classes — use real instances because they are cheap to construct
- Use
Thread.sleep() in coroutine tests — use advanceTimeBy for deterministic timing
- Skip the RED phase in TDD — if the test passes before implementation, the test is wrong
- Test private functions directly — test through the public API instead
CI/CD Integration
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
- run: ./gradlew test koverXmlReport
- run: ./gradlew koverVerify
Reference Files
- kotest-examples.md — StringSpec, FunSpec, BehaviorSpec, DescribeSpec full examples
- mockk-examples.md — Mocking, coroutine mocking, argument capture, spying
- coroutine-testing.md — runTest, Flow testing, TestDispatcher
- advanced-testing.md — Property-based, data-driven, fixtures, extensions, Ktor testApplication