Skip to content

What it is

Kotest is a Kotlin testing framework with multiple spec styles, a large assertion library, property-based testing and data-driven test support.

Choose a spec style — StringSpec, DescribeSpec, BehaviorSpec and others — and use shouldBe style matchers. Property testing generates inputs automatically.

Installation

testImplementation("io.kotest:kotest-runner-junit5:5.9.0")

Getting started

The smallest useful thing you can do with it, and what each part means.

Describe-style specs and matchers
class BookServiceTest : DescribeSpec({
    describe("BookService") {
        val repo = mockk<BookRepository>()
        val service = BookService(repo)

        describe("getTitle") {
            it("returns the title when the book exists") {
                coEvery { repo.findById(42) } returns Book(42, "Dune")
                service.getTitle(42) shouldBe "Dune"
            }

            it("throws when the book is missing") {
                coEvery { repo.findById(99) } returns null
                shouldThrow<NotFoundException> { service.getTitle(99) }
            }
        }
    }
})

// Rich matchers
books shouldHaveSize 3
books.shouldContainExactlyInAnyOrder(a, b, c)
result.shouldBeInstanceOf<Success>()
value shouldBeInRange 1..10
Nested describe blocks produce readable output that names the behaviour being tested, which makes a failing test in CI self-explanatory.

Advanced usage

Where the library earns its place over a simpler alternative.

Property and data-driven testing
class SlugifyTest : StringSpec({
    "slugs contain only safe characters" {
        checkAll(Arb.string()) { input ->
            slugify(input).forAll { c -> c.isLetterOrDigit() || c == '-' }
        }
    }

    "round trips" {
        checkAll(Arb.int(), Arb.string(1..50)) { id, title ->
            val encoded = encode(Book(id, title))
            decode(encoded) shouldBe Book(id, title)
        }
    }
})

class RangeTest : FunSpec({
    withData(
        nameFn = { "year ${it.first} is ${if (it.second) "valid" else "invalid"}" },
        1399 to false, 1400 to true, 2026 to true, 3000 to false,
    ) { (year, expected) -> isValidYear(year) shouldBe expected }
})
checkAll generates hundreds of inputs and shrinks any failure to a minimal case — it finds the empty string and Unicode edge cases that example tests never cover.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

No tests are discovered
The JUnit 5 platform is not enabled. Add useJUnitPlatform() to the Gradle test task.
Shared state leaks between tests
Specs are instantiated once per class by default. Set isolationMode to InstancePerLeaf for fresh state per test.

Best practices

  • Pick one spec style per project; mixing them makes the suite hard to read.
  • Use property tests for pure functions with invariants — encoding, parsing, validation.
  • Use withData for table-driven cases instead of near-duplicate test functions.
  • Kotest assertions work with JUnit too, if you want the matchers without switching runners.

Background

Why it exists, and what it was reacting to.

Kotest offers what JUnit does not attempt: nested descriptive test structures, powerful matchers and generative testing, all in idiomatic Kotlin.