ECC Kotlin 测试规则实战:.cursor/rules/kotlin-testing.md 如何驱动 Kotest、MockK 与 Kover 工作流
【免费下载链接】ECCThe agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.项目地址: https://gitcode.com/GitHub_Trending/ev/ECC
本文以 ECC 仓库中的 Cursor 规则文件 .cursor/rules/kotlin-testing.md 为核心,解析这条规则如何在全局测试基线之上叠加 Kotlin 专属约束,并沿规则给出的三条主线——“Kotest 框架选型 + MockK 模拟”“runTest协程测试”“Kover 覆盖率验证”——展开为完整可运行的代码示例与验证命令。读完后,你将掌握一套规则驱动的 Kotlin 测试工作流:选择合适的 Spec 风格、用 MockK 隔离依赖、在虚拟时间中测试协程与 Flow、执行 TDD 红-绿-重构循环,并用 Kover 把覆盖率门槛固化为构建失败条件。
1. 规则文件本体:Frontmatter、生效范围与继承关系
1.1 文件位置与 Frontmatter 机制
kotlin-testing.md是 ECC 面向 Cursor 的“按需加载规则”。其文件头完整继承如下:
--- description: "Kotlin testing extending common rules" globs: ["**/*.kt", "**/*.kts", "**/build.gradle.kts"] alwaysApply: false ---三个字段共同决定了这条规则的注入时机:
| 字段 | 取值 | 作用 |
|---|---|---|
description | Kotlin testing extending common rules | 向 Agent 声明规则主题:它是“对通用测试规则的 Kotlin 扩展”,而非独立体系 |
globs | **/*.kt、**/*.kts、**/build.gradle.kts | 只有当 Agent 正在处理 Kotlin 源码、Kotlin 脚本或 Kotlin 版 Gradle 构建文件时,规则才被匹配加载 |
alwaysApply | false | 不全局常驻上下文,节省 token 预算,实现“按文件类型懒加载” |
从仓库的 安装脚本 可以确认cursor安装组件的职责:“Install rules, hooks, and bundled Cursor configs to./.cursor/”。也就是说,这份规则文件是 ECC 安装流程的产物,随rules资产整体落盘到项目根目录的.cursor/rules/下,供 Cursor 按上述 glob 匹配策略消费。
1.2 与通用测试基线的继承关系
规则正文第一行声明:“This file extends the common testing rule with Kotlin-specific content.”,它扩展的对象是 .cursor/rules/common-testing.md。该基线规则是alwaysApply: true(全局生效),规定了三个硬约束:
- 最低覆盖率 80%,且单元测试、集成测试、E2E 测试三类全部必需;
- TDD 是强制工作流:写测试(RED)→ 运行确认失败 → 写最小实现(GREEN)→ 运行确认通过 → 重构 → 验证覆盖率 80%+;
- 失败排查顺序:先用
tdd-guideagent 定位,再检查测试隔离与 mock 正确性,原则上“修实现,不修测试”。
kotlin-testing.md的角色因此非常清晰:它不重复覆盖率与 TDD 基线,只回答“在 Kotlin 生态里用什么工具落地这些要求”——答案是Kotest + MockK + kotlinx-coroutines-test + Kover。
2. 框架选型:Kotest Spec 风格 + MockK
规则原文给出的框架结论只有一句话:
UseKotestwith spec styles (StringSpec, FunSpec, BehaviorSpec) andMockKfor mocking.
这句话排除了 JUnit + Mockito 路线,把断言语言统一为 Kotest matcher、把模拟层统一为 MockK。ECC 配套的 kotlin-testing skill 对每种 Spec 风格给出了可直接复用的形态,可以按测试语义选择:
StringSpec(最简)—— 一行描述一个测试,适合纯函数与工具类:
class CalculatorTest : StringSpec({ "add two positive numbers" { Calculator.add(2, 3) shouldBe 5 } "add negative numbers" { Calculator.add(-1, -2) shouldBe -3 } })FunSpec(JUnit 风格)——test { }块式结构,最适合配合 MockK 的coEvery测试服务层:
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)——Given/When/Then结构,适合业务规则验证(例如下单、支付失败分支):
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 风格)—— 用describe/context/it描述“某函数在某种输入下的行为”,适合校验器一类的边界逻辑:
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() } } } })skill 同时强调一个组织纪律:同一项目内保持 Spec 风格一致,避免四风格混用。
2.1 断言语言:Kotest Matchers
Kotest matcher 是规则选型的另一半,覆盖了常见断言场景:
// 相等 result shouldBe expected result shouldNotBe unexpected // 字符串 name shouldStartWith "Al" name shouldMatch Regex("[A-Z][a-z]+") // 集合 list shouldContain "item" list shouldHaveSize 3 list.shouldBeSorted() // 空值与类型 result.shouldNotBeNull() result.shouldBeInstanceOf<User>() // 数值 count shouldBeGreaterThan 0 price shouldBeInRange 1.0..100.0 // 异常 shouldThrow<IllegalArgumentException> { validateAge(-1) }.message shouldBe "Age must be positive"对于领域语义较强的断言,可以写自定义 matcher复用业务谓词:
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" }, ) } // 使用 user should beActiveUser()2.2 MockK:基础模拟、参数捕获与 Spy
MockK 在 Kotlin 项目中的核心优势是原生支持挂起函数(coEvery/coVerify)。skill 给出的基础用法包含relaxedmock、beforeTest清理和verify调用次数校验:
class UserServiceTest : FunSpec({ val repository = mockk<UserRepository>() val logger = mockk<Logger>(relaxed = true) // relaxed:未打桩的调用返回默认值 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") } } })两个高频进阶模式:
参数捕获—— 用slot+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 / 部分打桩—— 只覆写个别方法(如随机 ID 生成),其余走真实实现:
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() } // 仅该方法被覆写 }3. 协程测试:runTest是规则钦定的唯一入口
规则原文给出的协程测试范式是kotlinx-coroutines-test的runTest:
test("async operation completes") { runTest { val result = service.fetchData() result.shouldNotBeEmpty() } }runTest的价值在于提供一个带虚拟时间的TestScope:delay不会真实等待墙钟,而是由测试调度器按虚拟时间推进,这使得耗时逻辑可以被毫秒级地测完。仓库内 KMP/Android 方向的姊妹规则 rules/kotlin/testing.md 对此也有一致表述:“UserunTest— it auto-advances virtual time and providesTestScope”,两处规则在这一点上是互相印证的。
3.1 测试 Flow:收集、数量断言与防抖
Flow 测试的标准动作是“限定收集数量 + 虚拟时间推进”:
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") // 只有这次应触发搜索 advanceTimeBy(500) results shouldHaveSize 1 job.cancel() } }第二个例子同时演示了advanceTimeBy的正确用法:连续三次emit中只有最后一次应该触发搜索,这正是防抖语义的断言——这也呼应了 skill 中 DON'T 清单里的“不要在协程测试中使用Thread.sleep(),用advanceTimeBy代替”。
3.2 挂起函数的 Mock 与超时行为
MockK 的coEvery打桩挂起函数,并可用coAnswers模拟真实延迟;超时行为则用withTimeout断言:
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("timeout after delay") { runTest { val service = SlowService() shouldThrow<TimeoutCancellationException> { withTimeout(100) { service.slowOperation() // 耗时 > 100ms(虚拟时间) } } } }4. 覆盖率:Kover 的两条命令与完整 Gradle 配置
规则原文给出的覆盖率操作是两条命令:
./gradlew koverHtmlReport ./gradlew koverVerifykoverHtmlReport:跑测试并生成 HTML 报告,产物位于build/reports/kover/html/index.html;koverVerify:按预设阈值校验覆盖率,不达标时让构建失败——这是把“80% 最低覆盖率”这条通用基线从口号变成 CI 门禁的关键一步。
ECC 在 kotlin-testing skill 中给出了与这两条命令配套的完整build.gradle.kts配置,报告格式、排除规则、阈值三处都要配齐:
// 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) // 覆盖率低于 80% 时构建失败 } } } }配套命令与查看方式:
# 带覆盖率运行测试 ./gradlew koverHtmlReport # 校验覆盖率阈值 ./gradlew koverVerify # 生成 XML 报告(供 CI 消费) ./gradlew koverXmlReport # 查看 HTML 报告(按操作系统选择命令) # macOS: open build/reports/kover/html/index.html # Linux: xdg-open build/reports/kover/html/index.html # Windows: start build/reports/kover/html/index.htmlskill 还给出了分代码类型的覆盖率目标,与“80% 全局底线”分层对应:
| 代码类型 | 目标 |
|---|---|
| 关键业务逻辑 | 100% |
| 公开 API | 90%+ |
| 一般代码 | 80%+ |
| 生成/配置代码 | 从统计中排除 |
注意excludes与上表最后一行是配套的:*.generated.*、*.config.*不参与覆盖率统计,否则生成代码会稀释真实业务的覆盖率数字。
5. 规则指向前方:kotlin-testingskill 的完整能力地图
规则末尾的 Reference 段写着:“See skill:kotlin-testingfor detailed Kotest patterns, MockK usage, and property-based testing.”。这个引用指向仓库中的 skills/kotlin-testing/SKILL.md,它把规则里的一句话选型展开成了可执行的方法论,包含以下规则正文未展开、但实战必需的内容。
5.1 TDD 全周期示例:EmailValidator 的 RED-GREEN-REFACTOR
skill 用一个Result风格的校验器走完了通用基线要求的 TDD 六步:
// 第 1 步:只定义签名 fun validateEmail(email: String): Result<String> { TODO("not implemented") } // 第 2 步:写会失败的测试(RED) 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() } }) // ./gradlew test → 应看到 NotImplementedError,确认 RED // 第 4 步:最小实现(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) } // ./gradlew test → 三个用例全部 PASSED,第 6 步再重构并复验5.2 数据驱动与属性化测试
withData数据驱动:同一断言模板套多个输入/期望对,无效输入分支还能用nameFn为每条用例生成可读名称:
context("parsing valid dates") { withData( "2026-01-15" to LocalDate(2026, 1, 15), "2026-12-31" to LocalDate(2026, 12, 31), ) { (input, expected) -> parseDate(input) shouldBe expected } } context("rejecting invalid dates") { withData( nameFn = { "rejects '$it'" }, "not-a-date", "2026-13-01", "", ) { input -> shouldThrow<DateParseException> { parseDate(input) } } }属性化测试:对纯函数用forAll/checkAll表达不变式,序列化往返是否保真是典型适用场景:
test("string reverse is involutory") { forAll<String> { s -> s.reversed().reversed() == s } } 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 } }自定义生成器用Arb.bind组合字段级任意值,可复用到多个 spec。
5.3 生命周期、Fixture 与扩展
数据库类测试用beforeSpec/afterSpec管理规格级资源,用beforeTest保证用例间隔离;跨多个 spec 复用的资源则封装成 Kotest 扩展:
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" } })5.4 Ktor 服务测试与常用命令
对 Ktor 后端,skill 给出的集成测试入口是testApplication,直接在进程内构建应用并发起真实 HTTP 语义的请求:
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() } } })skill 末尾还给出了一张测试期常用命令速查表:
./gradlew test # 全量测试 ./gradlew test --tests "com.example.UserServiceTest" # 单测类 ./gradlew test --tests "com.example.UserServiceTest.getUser returns user when found" # 单用例 ./gradlew test --info # 详细输出 ./gradlew koverHtmlReport # 带覆盖率 ./gradlew detekt # 静态分析 ./gradlew ktlintCheck # 格式检查 ./gradlew test --continuous # 连续测试5.5 最佳实践与 CI 门禁
skill 的 DO/DON'T 清单把前述要点收敛为团队纪律:
- DO:先写测试(TDD);全项目统一 Spec 风格;挂起函数一律
coEvery/coVerify;协程测试用runTest;测行为不测实现;纯函数优先属性化测试。 - DON'T:混用测试框架;mock
data class(应构造真实实例);协程测试里用Thread.sleep();跳过 RED 阶段;直接测私有函数;无视 flaky 测试。
CI 侧的最小门禁(skill 中的 GitHub Actions 示例)是两步 Gradle 命令 + 覆盖率上传:
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 }}6. 补充视角:面向 KMP/Android 的姊妹规则
仓库中还有一份 rules/kotlin/testing.md,面向 Kotlin Multiplatform 与 Android 场景,与本文的规则文件构成互补:它推荐kotlin.test(KMP 通用)与 JUnit(Android 专属)、用Turbine测Flow/StateFlow(test { awaitItem() }风格)、明确“手写 Fake 优先于 mock 框架”、给出 KtorMockEngine与 SQLDelight 内存驱动(JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY))的集成测试写法,并要求“每个功能的 ViewModel + UseCase 至少要有测试”。如果你的项目同时涉及 Android 侧,两份规则应一并参考;纯 JVM/Ktor 后端则以本文的kotlin-testing.md规则及其指向的 skill 为准。
7. 小结:一条规则串起的完整测试链路
kotlin-testing.md的正文虽然精简,但它通过“扩展通用基线 + 引用 skill”的方式,把整条链路钉死了:
- 基线层(common-testing.md):80% 覆盖率、三类测试、TDD 强制循环;
- 规则层(kotlin-testing.md):Kotest spec 风格 + MockK、
runTest、koverHtmlReport/koverVerify; - 能力层(skills/kotlin-testing/SKILL.md):四种 Spec 风格示例、matcher 与自定义 matcher、MockK 参数捕获与 Spy、Flow/虚拟时间测试、
withData与属性化测试、Kover 完整 Gradle 配置、KtortestApplication、命令速查表与 CI 门禁。
| 关注点 | 落地方式 | 依据 |
|---|---|---|
| 规则按需生效 | frontmatterglobs+alwaysApply: false | .cursor/rules/kotlin-testing.md |
| 规则安装 | cursor组件写入./.cursor/ | scripts/install-apply.js |
| 框架选型 | Kotest(StringSpec/FunSpec/BehaviorSpec/DescribeSpec)+ MockK | .cursor/rules/kotlin-testing.md |
| 协程测试 | kotlinx-coroutines-test的runTest | .cursor/rules/kotlin-testing.md、rules/kotlin/testing.md |
| 覆盖率门禁 | KoverkoverVerify+minBound(80) | skills/kotlin-testing/SKILL.md |
| 深度模式 | TDD 示例、属性化测试、Ktor 测试、CI 配置 | skills/kotlin-testing/SKILL.md |
【免费下载链接】ECCThe agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.项目地址: https://gitcode.com/GitHub_Trending/ev/ECC
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考