跳到正文
cccskills
免费在 GitHub 公开中文介绍

kotlin-testing

为Kotlin代码先编写测试,使用Kotest和MockK验证行为与依赖交互,涵盖异步测试、生成输入的属性测试及Kover覆盖率检查。

原文查看原文说明

Kotlin testing patterns with Kotest, MockK, coroutine testing, property-based testing, and Kover coverage. Follows TDD methodology with idiomatic Kotlin practices. Use when writing Kotlin tests with Kotest or MockK, or testing coroutines and checking coverage.

AI 评分90分/ 100查看安装方法

适用场景

  • 为现有Kotlin代码补充测试
  • 先写测试再开发新功能
  • 验证协程和Flow的行为
  • 检查测试覆盖率与达标情况

中文介绍

功能

为Kotlin代码编写、运行和改进测试,并检查覆盖率。使用Kotest编写验证内容,通过MockK用测试替身替换依赖,检查目标代码的行为。遵循先写测试的测试驱动开发方式,反复执行确认测试失败、编写最少实现、在保持行为的前提下重构代码的循环。还涵盖协程异步代码、Flow、用大量生成输入验证性质的属性测试,以及Ktor的API测试。

适用场景

适合从测试开始开发新函数或类,也适合为现有代码增加验证。需要控制异步测试中的等待时间,或通过Kover了解测试执行覆盖的代码比例时,也可以使用。

使用示例

  • “请为这个Kotlin函数添加Kotest测试。”
  • “请用MockK替换依赖,并验证协程的行为。”
  • “请配置Kover,检查覆盖率是否达到80%。”

注意事项

需要根据所用功能添加Kotest、MockK、异步测试库和Kover等依赖。运行示例使用Gradle。资料以普通代码覆盖率至少80%为目标,并排除生成代码和配置代码。

本介绍由 AI(OpenAI Codex)根据公开的 SKILL.md 撰写。准确的规格请查看下方原文。

客观评估

AI 仅根据 SKILL.md 的内容,从 4 个方面进行了评估。星标数和热度不计入评估。

指令清晰度能否看出何时使用、做什么
A良好明确说明适用于Kotlin新开发、现有代码补测及测试驱动开发流程。
步骤具体性步骤、输入输出和示例是否具体
A良好提供从确认失败到实现、重构的步骤,以及Kotest代码示例和Gradle命令。
安全性发送数据、删除文件、执行命令等是否有说明
A良好将代码修改与执行限定为测试、实现和重构,并列出Gradle运行示例。
安装难易度所需工具和 API 密钥是否少且有说明
B部分不足列出Kotest、MockK、Kover和Gradle,但所提供节选没有依赖安装配置。

总分按每项 A=25 分、B=15 分、C=5 分相加。 由 AI(OpenAI Codex)于 2026年10月19日 评估。仅供参考,安装前请同时查看原文。

如果您认为评估有误,请申请复核

请写明哪一项有误以及原因。无需登录。我们会对照 SKILL.md 重新评估,并在此处更新结果。

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

包含的文件(1)

  • SKILL.md20.3 KB

SKILL.md(原文)

安装前可以查看交给 AI 代理的指令内容。

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

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 requirement

Step-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 pass

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

Coverage Targets

Code TypeTarget
Critical business logic100%
Public APIs90%+
General code80%+
Generated / config codeExclude

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

Best Practices

DO:

  • Write tests FIRST (TDD)
  • Use Kotest's spec styles consistently across the project
  • Use MockK's coEvery/coVerify for suspend functions
  • Use runTest for coroutine testing
  • Test behavior, not implementation
  • Use property-based testing for pure functions
  • Use data class test 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 (use advanceTimeBy)
  • 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.

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

同一仓库的技能

Design and audit interfaces for Web, iOS, and Android using WCAG 2.2 accessibility guidance. Check screen-reader labels, keyboard navigation, contrast, and control sizes to identify barriers and guide implementation for people using assistive technology.

中文简介正在准备中。当前显示英语简介。

  • Adding descriptive labels to icon-only buttons
  • Reviewing keyboard navigation and modal focus
  • Checking contrast and control sizes
affaan-m/ECC28.3万2026年10月16日 更新

Investigate unreliable AI agents across 12 layers, including instructions, memory, tool execution, and display. Use code and logs to trace failures, rank findings by severity, and produce an ordered repair plan that prioritizes code changes.

中文简介正在准备中。当前显示英语简介。

  • Investigating failures that appear only inside an agent application
  • Tracing old conversation content leaking into new sessions
  • Checking skipped or falsely claimed tool execution
affaan-m/ECC28.3万2026年10月16日 更新

agent-eval

免费有简介AI 评分 80分

用实际开发任务比较多个编程智能体,记录成功率、可获取的API费用、耗时和重复运行的稳定性,为工具选型及更新后的性能评估提供数据。

  • 用实际开发任务比较智能体
  • 采用新工具或模型前进行评估
  • 检查智能体更新后的性能
affaan-m/ECC28.3万2026年10月16日 更新

Design and refine an AI agent’s available actions, tool inputs, and result formats. Define recovery steps for errors, manage the information kept in context, and evaluate changes through completion rates, retries, and cost per successful task.

中文简介正在准备中。当前显示英语简介。

  • Define an AI agent’s tool set and input formats
  • Make tool results and next steps easier to interpret
  • Specify safe retries and stopping conditions
affaan-m/ECC28.3万2026年10月16日 更新

Diagnose AI agent runs that fail repeatedly, loop through tools, or lose track of the task. Capture errors and environment details, test a focused recovery action, and produce a report explaining the evidence and outcome.

中文简介正在准备中。当前显示英语简介。

  • Diagnosing repeated failures in an agent run
  • Investigating loops through the same tool calls
  • Reviewing output degradation as context grows
affaan-m/ECC28.3万2026年10月5日 更新

agent-introspection-debugging

免费有简介AI 评分 100分

在AI代理反复失败或重复操作时,记录错误与环境前提、排查原因,采取影响有限的恢复操作,并汇总结果和证据,为有依据的重试与交接提供支持。

  • 排查重复调用工具的原因
  • 调查对话膨胀导致的质量下降
  • 核对文件路径与环境差异
affaan-m/ECC28.3万2026年10月16日 更新

查看 affaan-m 的全部技能

报告此技能的问题