# Kotlin Testing

> Kotlin testing patterns with Kotest, MockK, coroutine testing, property-based testing, and Kover coverage. Follows TDD methodology with idiomatic Kotlin practices.

- Skill: `lidge-jun/kotlin-testing` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add lidge-jun/kotlin-testing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lidge-jun/kotlin-testing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: lidge-jun (https://skillmd.com/u/lidge-jun)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lidge-jun/kotlin-testing

---


# 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

1. **Identify target code** — Find the function, class, or module to test
2. **Write a Kotest spec** — Choose a spec style (FunSpec, StringSpec, BehaviorSpec, DescribeSpec) matching the test scope
3. **Mock dependencies** — Use MockK to isolate the unit under test
4. **Run tests (RED)** — Verify the test fails with the expected error
5. **Implement code (GREEN)** — Write minimal code to pass the test
6. **Refactor** — Improve the implementation while keeping tests green
7. **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)

```kotlin
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

```kotlin
// 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

```kotlin
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:

```kotlin
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.

```kotlin
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

```kotlin
// 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

```bash
./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

```bash
./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 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 class`es** — 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.

