kotlin-testing

star 4

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

lidge-jun By lidge-jun schedule Updated 3/22/2026

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

  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)

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

Install via CLI
npx skills add https://github.com/lidge-jun/cli-jaw-skills --skill kotlin-testing
Repository Details
star Stars 4
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator