
Kotlin Testing
- 5.4k installs
- 238k repo stars
- Updated August 5, 2026
- affaan-m/everything-claude-code
kotlin-testing is an agent skill that writes idiomatic Kotlin tests using Kotest, MockK, coroutine testing, property-based checks, and Kover coverage with TDD.
About
The kotlin-testing skill teaches comprehensive Kotlin testing with Kotest, MockK, coroutine testing, property-based testing, and Kover coverage following TDD methodology. Use when writing new Kotlin functions, adding coverage, implementing property tests, following red-green-refactor workflows, or configuring Kover thresholds. Workflow: identify target code, choose a Kotest spec style such as StringSpec, FunSpec, BehaviorSpec, or DescribeSpec, mock dependencies with MockK, run failing tests, implement minimal code, refactor, then verify coverage. Covers matchers, coEvery and coVerify for suspend functions, runTest with advanceTimeBy, Flow debounce testing, property forAll generators, withData tables, beforeSpec fixtures, spyk partial mocks, and Ktor testApplication HTTP tests. Kover enforces 80 percent minimum via koverVerify in CI with HTML and XML reports. Best practices mandate tests first, one Kotest style per project, behavior over implementation, and no Thread.sleep in coroutine tests.
- Red-green-refactor TDD walkthrough with EmailValidator from failing Kotest StringSpec to passing implementation.
- Kotest spec styles: StringSpec, FunSpec, BehaviorSpec, DescribeSpec with expressive matchers and custom matchers.
- MockK patterns: coEvery, coVerify, argument capture, relaxed mocks, and spyk partial overrides.
- Coroutine and Flow testing via runTest, advanceTimeBy, StandardTestDispatcher, and debounce assertions.
- Kover Gradle plugin with 80 percent verify rule plus Ktor testApplication HTTP route examples.
Kotlin Testing by the numbers
- 5,416 all-time installs (skills.sh)
- +251 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #268 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
kotlin-testing capabilities & compatibility
- Capabilities
- kotest spec style selection and expressive match · mockk mocking with coroutine coevery and argumen · runtest coroutine and flow debounce testing · property based forall and withdata table driven · kover coverage reports and ktor testapplication
- Works with
- github
- Use cases
- testing · api development · refactoring
What kotlin-testing says it does
Comprehensive Kotlin testing patterns for writing reliable, maintainable tests following TDD methodology with Kotest and MockK.
RED -> Write a failing test first
npx skills add https://github.com/affaan-m/everything-claude-code --skill kotlin-testingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5.4k |
|---|---|
| repo stars | ★ 238k |
| Security audit | 3 / 3 scanners passed |
| Last updated | August 5, 2026 |
| Repository | affaan-m/everything-claude-code ↗ |
How do I structure Kotest and MockK tests for Kotlin services, coroutines, Flows, and HTTP routes with measurable coverage in CI?
Kotlin TDD with Kotest spec styles, MockK mocking, coroutine and Flow tests, property-based checks, Kover coverage, and Ktor testApplication.
Who is it for?
Kotlin JVM or Ktor projects adding unit tests, coroutine coverage, property-based validation, or Kover-enforced coverage in Gradle CI.
Skip if: Skip when the stack is not Kotlin, when manual QA replaces automated tests, or when mixing multiple test frameworks in one project.
When should I use this skill?
User writes new Kotlin functions, adds test coverage, follows TDD in Kotlin, configures Kover, or tests Ktor routes with testApplication.
What you get
Passing red-green-refactor cycles, isolated mocks, coroutine-safe tests, property checks, and 80 percent plus Kover coverage verified in CI.
- Kotest spec test files
- Kover HTML and XML coverage reports
- CI workflow running test and koverVerify
By the numbers
- 80 percent minimum Kover coverage bound
- Four Kotest spec styles documented with examples
- Critical business logic target 100 percent coverage
Files
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 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 (StringSpec, FunSpec, BehaviorSpec) 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
Examples
The following sections contain detailed, runnable examples for each testing pattern:
Quick Reference
- Kotest specs — StringSpec, FunSpec, BehaviorSpec, DescribeSpec examples in Kotest Spec Styles
- Mocking — MockK setup, coroutine mocking, argument capture in MockK
- TDD walkthrough — Full RED/GREEN/REFACTOR cycle with EmailValidator in TDD Workflow for Kotlin
- Coverage — Kover configuration and commands in Kover Coverage
- Ktor testing — testApplication setup in Ktor testApplication Testing
TDD Workflow for Kotlin
The RED-GREEN-REFACTOR 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 requirementStep-by-Step TDD in Kotlin
// Step 1: Define the interface/signature
// EmailValidator.kt
package com.example.validator
fun validateEmail(email: String): Result<String> {
TODO("not implemented")
}
// Step 2: Write failing test (RED)
// EmailValidatorTest.kt
package com.example.validator
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.result.shouldBeFailure
import io.kotest.matchers.result.shouldBeSuccess
class EmailValidatorTest : StringSpec({
"valid email returns success" {
validateEmail("user@example.com").shouldBeSuccess("user@example.com")
}
"empty email returns failure" {
validateEmail("").shouldBeFailure()
}
"email without @ returns failure" {
validateEmail("userexample.com").shouldBeFailure()
}
})
// Step 3: Run tests - verify FAIL
// $ ./gradlew test
// EmailValidatorTest > valid email returns success FAILED
// kotlin.NotImplementedError: An operation is not implemented
// Step 4: Implement minimal code (GREEN)
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)
}
// Step 5: Run tests - verify PASS
// $ ./gradlew test
// EmailValidatorTest > valid email returns success PASSED
// EmailValidatorTest > empty email returns failure PASSED
// EmailValidatorTest > email without @ returns failure PASSED
// Step 6: Refactor if needed, verify tests still passKotest Spec Styles
StringSpec (Simplest)
class CalculatorTest : StringSpec({
"add two positive numbers" {
Calculator.add(2, 3) shouldBe 5
}
"add negative numbers" {
Calculator.add(-1, -2) shouldBe -3
}
"add zero" {
Calculator.add(0, 5) shouldBe 5
}
})FunSpec (JUnit-like)
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")
}
}
})BehaviorSpec (BDD Style)
class OrderServiceTest : BehaviorSpec({
val repository = mockk<OrderRepository>()
val paymentService = mockk<PaymentService>()
val service = OrderService(repository, paymentService)
Given("a valid order request") {
val request = CreateOrderRequest(
userId = "user-1",
items = listOf(OrderItem("product-1", quantity = 2)),
)
When("the order is placed") {
coEvery { paymentService.charge(any()) } returns PaymentResult.Success
coEvery { repository.save(any()) } answers { firstArg() }
val result = service.placeOrder(request)
Then("it should return a confirmed order") {
result.status shouldBe OrderStatus.CONFIRMED
}
Then("it should charge payment") {
coVerify(exactly = 1) { paymentService.charge(any()) }
}
}
When("payment fails") {
coEvery { paymentService.charge(any()) } returns PaymentResult.Declined
Then("it should throw PaymentException") {
shouldThrow<PaymentException> {
service.placeOrder(request)
}
}
}
}
})DescribeSpec (RSpec Style)
class UserValidatorTest : DescribeSpec({
describe("validateUser") {
val validator = UserValidator()
context("with valid input") {
it("accepts a normal user") {
val user = CreateUserRequest("Alice", "alice@example.com")
validator.validate(user).shouldBeValid()
}
}
context("with invalid name") {
it("rejects blank name") {
val user = CreateUserRequest("", "alice@example.com")
validator.validate(user).shouldBeInvalid()
}
it("rejects name exceeding max length") {
val user = CreateUserRequest("A".repeat(256), "alice@example.com")
validator.validate(user).shouldBeInvalid()
}
}
}
})Kotest Matchers
Core Matchers
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.string.*
import io.kotest.matchers.collections.*
import io.kotest.matchers.nulls.*
// 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)
}Custom Matchers
fun beActiveUser() = object : Matcher<User> {
override fun test(value: User) = MatcherResult(
value.isActive && value.lastLogin != null,
{ "User ${value.id} should be active with a last login" },
{ "User ${value.id} should not be active" },
)
}
// Usage
user should beActiveUser()MockK
Basic Mocking
class UserServiceTest : FunSpec({
val repository = mockk<UserRepository>()
val logger = mockk<Logger>(relaxed = true) // Relaxed: returns defaults
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("findUser returns null for unknown id") {
every { repository.findById(any()) } returns null
val result = service.findUser("unknown")
result.shouldBeNull()
}
})Coroutine Mocking
class AsyncUserServiceTest : FunSpec({
val repository = mockk<UserRepository>()
val service = UserService(repository)
test("getUser suspending function") {
coEvery { repository.findById("1") } returns User(id = "1", name = "Alice")
val result = service.getUser("1")
result.name shouldBe "Alice"
coVerify { repository.findById("1") }
}
test("getUser with delay") {
coEvery { repository.findById("1") } coAnswers {
delay(100) // Simulate async work
User(id = "1", name = "Alice")
}
val result = service.getUser("1")
result.name shouldBe "Alice"
}
})Argument Capture
test("save captures the user argument") {
val slot = slot<User>()
coEvery { repository.save(capture(slot)) } returns Unit
service.createUser(CreateUserRequest("Alice", "alice@example.com"))
slot.captured.name shouldBe "Alice"
slot.captured.email shouldBe "alice@example.com"
slot.captured.id.shouldNotBeNull()
}Spy and Partial Mocking
test("spy on real object") {
val realService = UserService(repository)
val spy = spyk(realService)
every { spy.generateId() } returns "fixed-id"
spy.createUser(request)
verify { spy.generateId() } // Overridden
// Other methods use real implementation
}Coroutine Testing
runTest for Suspend Functions
import kotlinx.coroutines.test.runTest
class CoroutineServiceTest : FunSpec({
test("concurrent fetches complete together") {
runTest {
val service = DataService(testScope = this)
val result = service.fetchAllData()
result.users.shouldNotBeEmpty()
result.products.shouldNotBeEmpty()
}
}
test("timeout after delay") {
runTest {
val service = SlowService()
shouldThrow<TimeoutCancellationException> {
withTimeout(100) {
service.slowOperation() // Takes > 100ms
}
}
}
}
})Testing Flows
import io.kotest.matchers.collections.shouldContainInOrder
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
class FlowServiceTest : FunSpec({
test("observeUsers emits updates") {
runTest {
val service = UserFlowService()
val emissions = service.observeUsers()
.take(3)
.toList()
emissions shouldHaveSize 3
emissions.last().shouldNotBeEmpty()
}
}
test("searchUsers debounces input") {
runTest {
val service = SearchService()
val queries = MutableSharedFlow<String>()
val results = mutableListOf<List<User>>()
val job = launch {
service.searchUsers(queries).collect { results.add(it) }
}
queries.emit("a")
queries.emit("ab")
queries.emit("abc") // Only this should trigger search
advanceTimeBy(500)
results shouldHaveSize 1
job.cancel()
}
}
})TestDispatcher
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
class DispatcherTest : FunSpec({
test("uses test dispatcher for controlled execution") {
val dispatcher = StandardTestDispatcher()
runTest(dispatcher) {
var completed = false
launch {
delay(1000)
completed = true
}
completed shouldBe false
advanceTimeBy(1000)
completed shouldBe true
}
}
})Property-Based Testing
Kotest Property Testing
import io.kotest.core.spec.style.FunSpec
import io.kotest.property.Arb
import io.kotest.property.arbitrary.*
import io.kotest.property.forAll
import io.kotest.property.checkAll
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString
// Note: The serialization roundtrip test below requires the User data class
// to be annotated with @Serializable (from kotlinx.serialization).
class PropertyTest : FunSpec({
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()
}
}
test("serialization roundtrip preserves data") {
checkAll(Arb.bind(Arb.string(1..50), Arb.string(5..100)) { name, email ->
User(name = name, email = "$email@test.com")
}) { user ->
val json = Json.encodeToString(user)
val decoded = Json.decodeFromString<User>(json)
decoded shouldBe user
}
}
})Custom Generators
val userArb: Arb<User> = Arb.bind(
Arb.string(minSize = 1, maxSize = 50),
Arb.email(),
Arb.enum<Role>(),
) { name, email, role ->
User(
id = UserId(UUID.randomUUID().toString()),
name = name,
email = Email(email),
role = role,
)
}
val moneyArb: Arb<Money> = Arb.bind(
Arb.long(1L..1_000_000L),
Arb.enum<Currency>(),
) { amount, currency ->
Money(amount, currency)
}Data-Driven Testing
withData in Kotest
class ParserTest : FunSpec({
context("parsing valid dates") {
withData(
"2026-01-15" to LocalDate(2026, 1, 15),
"2026-12-31" to LocalDate(2026, 12, 31),
"2000-01-01" to LocalDate(2000, 1, 1),
) { (input, expected) ->
parseDate(input) shouldBe expected
}
}
context("rejecting invalid dates") {
withData(
nameFn = { "rejects '$it'" },
"not-a-date",
"2026-13-01",
"2026-00-15",
"",
) { input ->
shouldThrow<DateParseException> {
parseDate(input)
}
}
}
})Test Lifecycle and Fixtures
BeforeTest / AfterTest
class DatabaseTest : FunSpec({
lateinit var db: Database
beforeSpec {
db = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1")
transaction(db) {
SchemaUtils.create(UsersTable)
}
}
afterSpec {
transaction(db) {
SchemaUtils.drop(UsersTable)
}
}
beforeTest {
transaction(db) {
UsersTable.deleteAll()
}
}
test("insert and retrieve user") {
transaction(db) {
UsersTable.insert {
it[name] = "Alice"
it[email] = "alice@example.com"
}
}
val users = transaction(db) {
UsersTable.selectAll().map { it[UsersTable.name] }
}
users shouldContain "Alice"
}
})Kotest Extensions
// Reusable test extension
class DatabaseExtension : BeforeSpecListener, AfterSpecListener {
lateinit var db: Database
override suspend fun beforeSpec(spec: Spec) {
db = Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1")
}
override suspend fun afterSpec(spec: Spec) {
// cleanup
}
}
class UserRepositoryTest : FunSpec({
val dbExt = DatabaseExtension()
register(dbExt)
test("save and find user") {
val repo = UserRepository(dbExt.db)
// ...
}
})Kover Coverage
Gradle Configuration
// 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
# Run tests with coverage
./gradlew koverHtmlReport
# Verify coverage thresholds
./gradlew koverVerify
# XML report for CI
./gradlew koverXmlReport
# View HTML report (use the command for your OS)
# macOS: open build/reports/kover/html/index.html
# Linux: xdg-open build/reports/kover/html/index.html
# Windows: start build/reports/kover/html/index.htmlCoverage Targets
| Code Type | Target |
|---|---|
| Critical business logic | 100% |
| Public APIs | 90%+ |
| General code | 80%+ |
| Generated / config code | Exclude |
Ktor testApplication Testing
class ApiRoutesTest : FunSpec({
test("GET /users returns list") {
testApplication {
application {
configureRouting()
configureSerialization()
}
val response = client.get("/users")
response.status shouldBe HttpStatusCode.OK
val users = response.body<List<UserResponse>>()
users.shouldNotBeEmpty()
}
}
test("POST /users creates user") {
testApplication {
application {
configureRouting()
configureSerialization()
}
val response = client.post("/users") {
contentType(ContentType.Application.Json)
setBody(CreateUserRequest("Alice", "alice@example.com"))
}
response.status shouldBe HttpStatusCode.Created
}
}
})Testing Commands
# Run all tests
./gradlew test
# Run specific test class
./gradlew test --tests "com.example.UserServiceTest"
# Run specific test
./gradlew test --tests "com.example.UserServiceTest.getUser returns user when found"
# Run with verbose output
./gradlew test --info
# Run with coverage
./gradlew koverHtmlReport
# Run detekt (static analysis)
./gradlew detekt
# Run ktlint (formatting check)
./gradlew ktlintCheck
# Continuous testing
./gradlew test --continuousBest Practices
DO:
- Write tests FIRST (TDD)
- Use Kotest's spec styles consistently across the project
- Use MockK's
coEvery/coVerifyfor suspend functions - Use
runTestfor coroutine testing - Test behavior, not implementation
- Use property-based testing for pure functions
- Use
data classtest fixtures for clarity
DON'T:
- Mix testing frameworks (pick Kotest and stick with it)
- Mock data classes (use real instances)
- Use
Thread.sleep()in coroutine tests (useadvanceTimeBy) - Skip the RED phase in TDD
- Test private functions directly
- Ignore flaky tests
Integration with CI/CD
# GitHub Actions example
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
- name: Run tests with coverage
run: ./gradlew test koverXmlReport
- name: Verify coverage
run: ./gradlew koverVerify
- name: Upload coverage
uses: codecov/codecov-action@v5
with:
files: build/reports/kover/report.xml
token: ${{ secrets.CODECOV_TOKEN }}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.
Related skills
Forks & variants (1)
Kotlin Testing has 1 known copy in the catalog totaling 1.4k installs. They canonicalize to this original listing.
- affaan-m - 1.4k installs
How it compares
Pick kotlin-testing for Kotest-MockK-Kover Kotlin stacks instead of generic unit-test skills that lack coroutine and property-based patterns.
FAQ
Which Kotest spec style should I pick?
Match scope: StringSpec for simple cases, FunSpec for JUnit-like tests, BehaviorSpec for BDD Given-When-Then, DescribeSpec for nested RSpec-style contexts.
How do I test Kotlin coroutines without flakiness?
Use runTest with advanceTimeBy or StandardTestDispatcher instead of Thread.sleep, and coEvery or coVerify for suspend function mocks.
What coverage threshold does kotlin-testing recommend?
Configure Kover with minBound 80 for general code, run koverHtmlReport locally, and koverVerify in CI with XML upload to codecov.
Is Kotlin Testing safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.