Communitygithub.com

Kotlin 测试模式

使用 Kotest、MockK、协程测试、属性测试和 Kover 覆盖率,遵循 TDD 方法论和惯用 Kotlin 实践的测试模式。适用于编写 Kotlin 测试、协程测试及覆盖率检查。

Kotlin 测试模式 是什么?

该技能为 Kotlin 开发者提供了一套完整的测试模式集合,涵盖 Kotest、MockK、协程测试、属性测试和 Kover 覆盖率,并严格遵循 TDD(测试驱动开发)方法论。它支持多种测试框架和样式,如 StringSpec、FunSpec、BehaviorSpec 和 DescribeSpec,并提供了丰富的匹配器(Matcher)和自定义匹配器。通过 MockK,开发者可以轻松模拟依赖、捕获参数和使用间谍对象(spy)。协程测试部分使用 runTest 和 TestDispatcher 来测试挂起函数、流和超时。属性测试基于 Kotest 属性库,支持自动生成数据和自定义生成器。Kover 用于代码覆盖率检查,确保 80% 以上的覆盖率。该技能适用于任何 Kotlin 项目,尤其是基于 Gradle 的 JVM 项目,帮助团队编写可靠、可维护的测试。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/kotlin-testing

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

Kotlin 测试模式 是做什么的?

一套全面的 Kotlin 测试模式,用于编写遵循 TDD 方法论的可靠、可维护的测试,使用 Kotest 和 MockK。

何时使用

  • 编写新的 Kotlin 函数或类
  • 为现有 Kotlin 代码添加测试覆盖
  • 实现属性测试
  • 在 Kotlin 项目中遵循 TDD 工作流程
  • 配置 Kover 进行代码覆盖率检查

工作原理

  1. 确定目标代码 — 找到要测试的函数、类或模块
  2. 编写 Kotest spec — 选择与测试范围匹配的 spec 样式(StringSpec、FunSpec、BehaviorSpec)
  3. 模拟依赖 — 使用 MockK 隔离被测单元
  4. 运行测试(RED) — 验证测试以预期错误失败
  5. 实现代码(GREEN) — 编写最少的代码以使测试通过
  6. 重构 — 在保持测试通过的同时改进实现
  7. 检查覆盖率 — 运行 ./gradlew koverHtmlReport 并验证 80%+ 的覆盖率

示例

以下部分包含了每种测试模式的详细、可运行示例:

快速参考

Kotlin TDD 工作流程

RED-GREEN-REFACTOR 周期

RED     -> 先编写失败的测试
GREEN   -> 编写最少的代码使测试通过
REFACTOR -> 在保持测试通过的同时改进代码
REPEAT  -> 继续下一个需求

Kotlin 中的逐步 TDD

// 步骤 1:定义接口/签名
// EmailValidator.kt
package com.example.validator

fun validateEmail(email: String): Result<String> {
    TODO("not implemented")
}

// 步骤 2:编写失败的测试(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({
    "有效邮箱返回 success" {
        validateEmail("[email protected]").shouldBeSuccess("[email protected]")
    }

    "空邮箱返回 failure" {
        validateEmail("").shouldBeFailure()
    }

    "不含 @ 的邮箱返回 failure" {
        validateEmail("userexample.com").shouldBeFailure()
    }
})

// 步骤 3:运行测试 - 验证失败
// $ ./gradlew test
// EmailValidatorTest > 有效邮箱返回 success FAILED
//   kotlin.NotImplementedError: An operation is not implemented

// 步骤 4:实现最少的代码(GREEN)
fun validateEmail(email: String): Result<String> {
    if (email.isBlank()) return Result.failure(IllegalArgumentException("邮箱不能为空"))
    if ('@' !in email) return Result.failure(IllegalArgumentException("邮箱必须包含 @"))
    val regex = Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
    if (!regex.matches(email)) return Result.failure(IllegalArgumentException("邮箱格式无效"))
    return Result.success(email)
}

// 步骤 5:运行测试 - 验证通过
// $ ./gradlew test
// EmailValidatorTest > 有效邮箱返回 success PASSED
// EmailValidatorTest > 空邮箱返回 failure PASSED
// EmailValidatorTest > 不含 @ 的邮箱返回 failure PASSED

// 步骤 6:如果需要,重构,并验证测试仍然通过

Kotest Spec 样式

StringSpec(最简单)

class CalculatorTest : StringSpec({
    "两个正数相加" {
        Calculator.add(2, 3) shouldBe 5
    }

    "负数相加" {
        Calculator.add(-1, -2) shouldBe -3
    }

    "加零" {
        Calculator.add(0, 5) shouldBe 5
    }
})

FunSpec(类 JUnit)

class UserServiceTest : FunSpec({
    val repository = mockk<UserRepository>()
    val service = UserService(repository)

    test("getUser 当找到时返回用户") {
        val expected = User(id = "1", name = "Alice")
        coEvery { repository.findById("1") } returns expected

        val result = service.getUser("1")

        result shouldBe expected
    }

    test("getUser 当未找到时抛出异常") {
        coEvery { repository.findById("999") } returns null

        shouldThrow<UserNotFoundException> {
            service.getUser("999")
        }
    }
})

BehaviorSpec(BDD 风格)

class OrderServiceTest : BehaviorSpec({
    val repository = mockk<OrderRepository>()
    val paymentService = mockk<PaymentService>()
    val service = OrderService(repository, paymentService)

    Given("一个有效的订单请求") {
        val request = CreateOrderRequest(
            userId = "user-1",
            items = listOf(OrderItem("product-1", quantity = 2)),
        )

        When("订单被提交") {
            coEvery { paymentService.charge(any()) } returns PaymentResult.Success
            coEvery { repository.save(any()) } answers { firstArg() }

            val result = service.placeOrder(request)

            Then("应返回已确认的订单") {
                result.status shouldBe OrderStatus.CONFIRMED
            }

            Then("应扣款") {
                coVerify(exactly = 1) { paymentService.charge(any()) }
            }
        }

        When("支付失败") {
            coEvery { paymentService.charge(any()) } returns PaymentResult.Declined

            Then("应抛出 PaymentException") {
                shouldThrow<PaymentException> {
                    service.placeOrder(request)
                }
            }
        }
    }
})

DescribeSpec(RSpec 风格)

class UserValidatorTest : DescribeSpec({
    describe("validateUser") {
        val validator = UserValidator()

        context("输入有效时") {
            it("接受正常的用户") {
                val user = CreateUserRequest("Alice", "[email protected]")
                validator.validate(user).shouldBeValid()
            }
        }

        context("名称无效时") {
            it("拒绝空白的名称") {
                val user = CreateUserRequest("", "[email protected]")
                validator.validate(user).shouldBeInvalid()
            }

            it("拒绝超过最大长度的名称") {
                val user = CreateUserRequest("A".repeat(256), "[email protected]")
                validator.validate(user).shouldBeInvalid()
            }
        }
    }
})

Kotest 匹配器

核心匹配器

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

// 相等性
result shouldBe expected
result shouldNotBe unexpected

// 字符串
name shouldStartWith "Al"
name shouldEndWith "ice"
name shouldContain "lic"
name shouldMatch Regex("[A-Z][a-z]+")
name.shouldBeBlank()

// 集合
list shouldContain "item"
list shouldHaveSize 3
list.shouldBeSorted()
list.shouldContainAll("a", "b", "c")
list.shouldBeEmpty()

// 空值
result.shouldNotBeNull()
result.shouldBeNull()

// 类型
result.shouldBeInstanceOf<User>()

// 数字
count shouldBeGreaterThan 0
price shouldBeInRange 1.0..100.0

// 异常
shouldThrow<IllegalArgumentException> {
    validateAge(-1)
}.message shouldBe "年龄必须为正数"

shouldNotThrow<Exception> {
    validateAge(25)
}

自定义匹配器

fun beActiveUser() = object : Matcher<User> {
    override fun test(value: User) = MatcherResult(
        value.isActive && value.lastLogin != null,
        { "用户 ${value.id} 应处于活跃状态并有上次登录时间" },
        { "用户 ${value.id} 不应处于活跃状态" },
    )
}

// 用法
user should beActiveUser()

MockK

基本模拟

class UserServiceTest : FunSpec({
    val repository = mockk<UserRepository>()
    val logger = mockk<Logger>(relaxed = true) // 宽松模式:返回默认值
    val service = UserService(repository, logger)

    beforeTest {
        clearMocks(repository, logger)
    }

    test("findUser 委托给 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 对未知 id 返回 null") {
        every { repository.findById(any()) } returns null

        val result = service.findUser("unknown")

        result.shouldBeNull()
    }
})

协程模拟

class AsyncUserServiceTest : FunSpec({
    val repository = mockk<UserRepository>()
    val service = UserService(repository)

    test("getUser 挂起函数") {
        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") {
        coEvery { repository.findById("1") } coAnswers {
            delay(100) // 模拟异步操作
            User(id = "1", name = "Alice")
        }

        val result = service.getUser("1")
        result.name shouldBe "Alice"
    }
})

参数捕获

test("save 捕获用户参数") {
    val slot = slot<User>()
    coEvery { repository.save(capture(slot)) } returns Unit

    service.createUser(CreateUserRequest("Alice", "[email protected]"))

    slot.captured.name shouldBe "Alice"
    slot.captured.email shouldBe "[email protected]"
    slot.captured.id.shouldNotBeNull()
}

间谍对象和部分模拟

test("间谍真实对象") {
    val realService = UserService(repository)
    val spy = spyk(realService)

    every { spy.generateId() } returns "fixed-id"

    spy.createUser(request)

    verify { spy.generateId() } // 被覆盖
    // 其他方法使用真实实现
}

协程测试

使用 runTest 测试挂起函数

import kotlinx.coroutines.test.runTest

class CoroutineServiceTest : FunSpec({
    test("并发获取同时完成") {
        runTest {
            val service = DataService(testScope = this)

            val result = service.fetchAllData()

            result.users.shouldNotBeEmpty()
            result.products.shouldNotBeEmpty()
        }
    }

    test("延迟后超时") {
        runTest {
            val service = SlowService()

            shouldThrow<TimeoutCancellationException> {
                withTimeout(100) {
                    service.slowOperation() // 耗时 > 100ms
                }
            }
        }
    }
})

测试 Flow

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 发出更新") {
        runTest {
            val service = UserFlowService()

            val emissions = service.observeUsers()
                .take(3)
                .toList()

            emissions shouldHaveSize 3
            emissions.last().shouldNotBeEmpty()
        }
    }

    test("searchUsers 防抖输入") {
        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") // 只有这个应触发搜索
            advanceTimeBy(500)

            results shouldHaveSize 1
            job.cancel()
        }
    }
})

TestDispatcher

import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle

class DispatcherTest : FunSpec({
    test("使用测试调度器进行受控执行") {
        val dispatcher = StandardTestDispatcher()

        runTest(dispatcher) {
            var completed = false

            launch {
                delay(1000)
                completed = true
            }

            completed shouldBe false
            advanceTimeBy(1000)
            completed shouldBe true
        }
    }
})

属性测试

Kotest 属性测试

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

// 注意:下面的序列化往返测试要求 User 数据类
// 使用 @Serializable 注解(来自 kotlinx.serialization)。

class PropertyTest : FunSpec({
    test("字符串反转是自逆的") {
        forAll<String> { s ->
            s.reversed().reversed() == s
        }
    }

    test("列表排序是幂等的") {
        forAll(Arb.list(Arb.int())) { list ->
            list.sorted() == list.sorted().sorted()
        }
    }

    test("序列化往返保留数据") {
        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
        }
    }
})

自定义生成器


…

[因翻译而截断]

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

相关技能