Claude
Skills
Sign in
Back

kotest

Included with Lifetime
$97 forever

Kotest — flexible, idiomatic Kotlin testing framework. Multiple specification styles (StringSpec, FunSpec, BehaviorSpec, DescribeSpec, FeatureSpec, FreeSpec), rich matcher library, property-based testing, data-driven tests, coroutine support, KMP-friendly. Drop-in alternative or complement to JUnit. USE WHEN: user mentions "Kotest", "io.kotest", "shouldBe", "StringSpec", "BehaviorSpec", "DescribeSpec", "kotest property testing", "Arb.list", "forAll", "kotest matchers", "kotlin tests" DO NOT USE FOR: JUnit-specific patterns - use junit skill (or framework-specific test skills) DO NOT USE FOR: Flow testing - use `testing/turbine` DO NOT USE FOR: Compose snapshot tests - use `testing/compose-snapshot` DO NOT USE FOR: Mobile E2E - use `testing/maestro`

Code Review

What this skill does

# Kotest

> **Deep Knowledge**: Use `mcp__documentation__fetch_docs` with technology: `kotest`.

## Why Kotest

| Feature | Kotest | JUnit 5 | Spek |
|---|---|---|---|
| Spec styles (BDD, FunSpec, StringSpec, etc.) | ✅ 9 styles | ❌ Single | ✅ |
| Rich matchers (`shouldBe`, `shouldContain`, `shouldThrow`) | ✅ Built-in | ❌ Need AssertJ/Hamcrest | ✅ |
| Property-based testing | ✅ Native | ❌ External | ❌ |
| Data-driven tests | ✅ `withData` | ✅ `@ParameterizedTest` | ❌ |
| Coroutine native (`runTest` via `coroutineScope`) | ✅ | ✅ Manual | ❌ |
| KMP support | ✅ Full | Partial (JVM only) | ❌ |
| Lifecycle hooks | ✅ Many | ✅ | ✅ |
| Test isolation modes | ✅ Configurable | ❌ Per-method | ❌ |
| Plugin ecosystem | ✅ Spring, Allure, Koin, MockK | ✅ | Limited |

## Setup

```kotlin
// build.gradle.kts
plugins {
    id("io.kotest.multiplatform") version "5.9.1"      // for KMP
    // OR
    kotlin("jvm")                                        // JVM-only
}

dependencies {
    testImplementation("io.kotest:kotest-runner-junit5:5.9.1")        // JVM
    testImplementation("io.kotest:kotest-assertions-core:5.9.1")
    testImplementation("io.kotest:kotest-property:5.9.1")             // property testing
    testImplementation("io.kotest:kotest-framework-datatest:5.9.1")   // data-driven
    testImplementation("io.kotest.extensions:kotest-extensions-koin:1.3.0")  // Koin plugin
}

// KMP (commonTest)
kotlin {
    sourceSets.commonTest.dependencies {
        implementation("io.kotest:kotest-framework-engine:5.9.1")
        implementation("io.kotest:kotest-assertions-core:5.9.1")
        implementation("io.kotest:kotest-property:5.9.1")
    }
}

tasks.withType<Test> {
    useJUnitPlatform()                                  // for JVM Kotest runs via JUnit Platform
}
```

## Spec Styles

### StringSpec (most idiomatic)

```kotlin
class WalletTest : StringSpec({
    "valid mnemonic creates wallet" {
        val wallet = Wallet.fromMnemonic("abandon abandon ...")
        wallet.address(0) shouldStartWith "bc1q"
    }

    "invalid mnemonic throws" {
        shouldThrow<InvalidMnemonicException> {
            Wallet.fromMnemonic("not enough words")
        }
    }
})
```

### FunSpec

```kotlin
class WalletTest : FunSpec({
    test("valid mnemonic creates wallet") {
        val wallet = Wallet.fromMnemonic(...)
        wallet.address(0) shouldStartWith "bc1q"
    }

    context("when balance is zero") {
        test("send fails") {
            shouldThrow<InsufficientFundsException> { wallet.send(...) }
        }
    }
})
```

### BehaviorSpec (BDD)

```kotlin
class WalletBehavior : BehaviorSpec({
    given("an empty wallet") {
        val wallet = Wallet.empty()
        `when`("sending 1000 sats") {
            then("throws InsufficientFundsException") {
                shouldThrow<InsufficientFundsException> { wallet.send(1000) }
            }
        }
    }

    given("a wallet with 5000 sats") {
        val wallet = Wallet.withBalance(5000)
        `when`("sending 1000 sats") {
            val result = wallet.send(1000)
            then("balance becomes 4000") { wallet.balance shouldBe 4000 }
            then("returns success") { result.isSuccess shouldBe true }
        }
    }
})
```

### DescribeSpec

```kotlin
class WalletDescribe : DescribeSpec({
    describe("Wallet") {
        describe("send()") {
            it("succeeds when sufficient funds") { /* ... */ }
            it("fails on insufficient funds") { /* ... */ }
        }
    }
})
```

### FreeSpec (deeply nested)

```kotlin
class WalletFree : FreeSpec({
    "Wallet" - {
        "send" - {
            "with sufficient funds" - {
                "succeeds" {
                    val wallet = Wallet.withBalance(5000)
                    wallet.send(1000).isSuccess shouldBe true
                }
            }
        }
    }
})
```

Pick **StringSpec** for most cases. Use **BehaviorSpec/DescribeSpec** when stakeholders read tests. **FunSpec** for nested setup with `context`.

## Matchers

```kotlin
// Equality
result shouldBe 42
result shouldNotBe 0
list shouldContainExactly listOf(1, 2, 3)
list shouldContainExactlyInAnyOrder listOf(3, 1, 2)
list shouldContain 5
list shouldHaveSize 3

// Strings
"hello world" shouldContain "world"
"abc" shouldStartWith "a"
"abc" shouldEndWith "c"
"abc" shouldMatch Regex("[a-z]+")
"abc" shouldHaveLength 3

// Collections
emptyList<Int>().shouldBeEmpty()
listOf(1, 2, 3).shouldContainAll(1, 2)
mapOf("a" to 1).shouldContainKey("a")

// Null
foo.shouldBeNull()
bar.shouldNotBeNull()

// Type
result.shouldBeInstanceOf<Success>()
result.shouldBeTypeOf<List<String>>()

// Booleans
flag.shouldBeTrue()
flag.shouldBeFalse()

// Numeric
balance shouldBeGreaterThan 0
balance shouldBeBetween 1000 to 5000
amount.toDouble() shouldBe (1.5 plusOrMinus 0.01)

// Throwable
shouldThrow<IllegalArgumentException> { invalid() }
val ex = shouldThrow<WalletException> { send() }
ex.message shouldContain "insufficient"

// Exact type
shouldThrowExactly<InsufficientFundsException> { /* not subclasses */ }

// Any matcher works in negative
result shouldNotBe null
"abc" shouldNotContain "xyz"
```

Inverse via `shouldNot`:
```kotlin
list shouldNot contain(0)
"abc" shouldNot startWith("z")
```

## Data-Driven Tests (`withData`)

```kotlin
class UriParserTest : FunSpec({
    context("BIP21 parser") {
        withData(
            "bitcoin:bc1q..." to BitcoinUri(address = "bc1q..."),
            "bitcoin:bc1q...?amount=0.1" to BitcoinUri(address = "bc1q...", amount = 10_000_000),
            "bitcoin:bc1q...?label=Test" to BitcoinUri(address = "bc1q...", label = "Test"),
        ) { (input, expected) ->
            BitcoinUri.parse(input) shouldBe expected
        }
    }
})
```

For named cases:

```kotlin
withData(
    nameFn = { "parses ${it.first} → ${it.second}" },
    "bitcoin:bc1q..." to BitcoinUri(...),
    "bitcoin:bc1q...?amount=0.1" to BitcoinUri(...),
) { (input, expected) ->
    BitcoinUri.parse(input) shouldBe expected
}
```

For test class data:

```kotlin
data class TestCase(val input: String, val expected: BitcoinUri)

withData(
    TestCase("bitcoin:bc1q...", BitcoinUri(...)),
    TestCase("bitcoin:bc1q...?amount=0.1", BitcoinUri(...)),
) { (input, expected) ->
    BitcoinUri.parse(input) shouldBe expected
}
```

## Property-Based Testing

```kotlin
import io.kotest.property.*
import io.kotest.property.arbitrary.*

class FeeCalculatorTest : StringSpec({
    "fee is always at least 1 sat/vbyte" {
        forAll<Int>(Arb.int(0..1_000_000)) { weight ->
            val fee = calculateFee(weight, feeRate = 1)
            fee >= weight / 4
        }
    }

    "fee scales linearly" {
        checkAll<Int, Int>(
            Arb.int(100..10_000),       // weight
            Arb.int(1..100),            // fee rate
        ) { weight, rate ->
            val fee = calculateFee(weight, rate)
            fee shouldBe (weight / 4 * rate)
        }
    }
})
```

### Arbitraries

```kotlin
Arb.int(0..100)
Arb.long()
Arb.double(0.0..1.0)
Arb.string(minSize = 1, maxSize = 100)
Arb.string(8..12, Codepoint.alphanumeric())
Arb.list(Arb.int(), 0..10)
Arb.set(Arb.string())
Arb.map(Arb.string(), Arb.int(), 0..10)

// Custom
data class User(val id: Long, val name: String)

val userArb = arbitrary {
    User(
        id = Arb.long().bind(),
        name = Arb.string(1..50).bind(),
    )
}

forAll<User>(userArb) { user ->
    user.id != 0L
}
```

### Shrinking

When a property fails, Kotest **shrinks** the input to find the smallest failing case automatically. Reported in test output.

## Lifecycle Hooks

```kotlin
class WalletTest : StringSpec({
    beforeSpec {
        // Once before all tests in spec
        Db.migrate()
    }

    beforeTest {
        // Before each test
        Db.clear()
    }

    afterTest { (test, result) ->
        // After each test
        if (result.isError) takeDebugSnapshot(test.name.testName)
    }

    afterSpec {
        Db.close()
    }

    "test 1" { /* ... */ }
    "test 2" { /* 

Related in Code Review