ECC Kotlin 测试规则实战:.cursor/rules/kotlin-testing.md 如何驱动 Kotest、MockK 与 Kover 工作流
发布时间:2026/9/7 4:31:04 作者:尧图编辑部 阅读量:1,286

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 ---三个字段共同决定了这条规则的注入时机字段取值作用descriptionKotlin testing extending common rules向 Agent 声明规则主题它是“对通用测试规则的 Kotlin 扩展”而非独立体系globs**/*.kt、**/*.kts、**/build.gradle.kts只有当 Agent 正在处理 Kotlin 源码、Kotlin 脚本或 Kotlin 版 Gradle 构建文件时规则才被匹配加载alwaysApplyfalse不全局常驻上下文节省 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 } })FunSpecJUnit 风格——test { }块式结构最适合配合 MockK 的coEvery测试服务层class UserServiceTest : FunSpec({ val repository mockkUserRepository() 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 shouldThrowUserNotFoundException { service.getUser(999) } } })BehaviorSpecBDD——Given/When/Then结构适合业务规则验证例如下单、支付失败分支class OrderServiceTest : BehaviorSpec({ val repository mockkOrderRepository() val paymentService mockkPaymentService() 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) { shouldThrowPaymentException { service.placeOrder(request) } } } } })DescribeSpecRSpec 风格—— 用describe/context/it描述“某函数在某种输入下的行为”适合校验器一类的边界逻辑class UserValidatorTest : DescribeSpec({ describe(validateUser) { val validator UserValidator() context(with valid input) { it(accepts a normal user) { val user CreateUserRequest(Alice, aliceexample.com) validator.validate(user).shouldBeValid() } } context(with invalid name) { it(rejects blank name) { val user CreateUserRequest(, aliceexample.com) validator.validate(user).shouldBeInvalid() } } } })skill 同时强调一个组织纪律同一项目内保持 Spec 风格一致避免四风格混用。2.1 断言语言Kotest MatchersKotest 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.shouldBeInstanceOfUser() // 数值 count shouldBeGreaterThan 0 price shouldBeInRange 1.0..100.0 // 异常 shouldThrowIllegalArgumentException { validateAge(-1) }.message shouldBe Age must be positive对于领域语义较强的断言可以写自定义 matcher复用业务谓词fun beActiveUser() object : MatcherUser { 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基础模拟、参数捕获与 SpyMockK 在 Kotlin 项目中的核心优势是原生支持挂起函数coEvery/coVerify。skill 给出的基础用法包含relaxedmock、beforeTest清理和verify调用次数校验class UserServiceTest : FunSpec({ val repository mockkUserRepository() val logger mockkLogger(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) } } })两个高频进阶模式参数捕获—— 用slotcapture校验“被测代码传给下游的对象内容”而不仅是“调用过没有”test(save captures the user argument) { val slot slotUser() coEvery { repository.save(capture(slot)) } returns Unit service.createUser(CreateUserRequest(Alice, aliceexample.com)) slot.captured.name shouldBe Alice slot.captured.email shouldBe aliceexample.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的runTesttest(async operation completes) { runTest { val result service.fetchData() result.shouldNotBeEmpty() } }runTest的价值在于提供一个带虚拟时间的TestScopedelay不会真实等待墙钟而是由测试调度器按虚拟时间推进这使得耗时逻辑可以被毫秒级地测完。仓库内 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 MutableSharedFlowString() val results mutableListOfListUser() 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 中 DONT 清单里的“不要在协程测试中使用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() shouldThrowTimeoutCancellationException { withTimeout(100) { service.slowOperation() // 耗时 100ms虚拟时间 } } } }4. 覆盖率Kover 的两条命令与完整 Gradle 配置规则原文给出的覆盖率操作是两条命令./gradlew koverHtmlReport ./gradlew koverVerifykoverHtmlReport跑测试并生成 HTML 报告产物位于build/reports/kover/html/index.htmlkoverVerify按预设阈值校验覆盖率不达标时让构建失败——这是把“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%公开 API90%一般代码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-REFACTORskill 用一个Result风格的校验器走完了通用基线要求的 TDD 六步// 第 1 步只定义签名 fun validateEmail(email: String): ResultString { TODO(not implemented) } // 第 2 步写会失败的测试RED class EmailValidatorTest : StringSpec({ valid email returns success { validateEmail(userexample.com).shouldBeSuccess(userexample.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): ResultString { 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 - shouldThrowDateParseException { parseDate(input) } } }属性化测试对纯函数用forAll/checkAll表达不变式序列化往返是否保真是典型适用场景test(string reverse is involutory) { forAllString { 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 $emailtest.com) }) { user - val json Json.encodeToString(user) val decoded Json.decodeFromStringUser(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] aliceexample.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.bodyListUserResponse() 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/DONT 清单把前述要点收敛为团队纪律DO先写测试TDD全项目统一 Spec 风格挂起函数一律coEvery/coVerify协程测试用runTest测行为不测实现纯函数优先属性化测试。DONT混用测试框架mockdata class应构造真实实例协程测试里用Thread.sleep()跳过 RED 阶段直接测私有函数无视 flaky 测试。CI 侧的最小门禁skill 中的 GitHub Actions 示例是两步 Gradle 命令 覆盖率上传test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-javav4 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-actionv5 with: files: build/reports/kover/report.xml token: ${{ secrets.CODECOV_TOKEN }}6. 补充视角面向 KMP/Android 的姊妹规则仓库中还有一份 rules/kotlin/testing.md面向 Kotlin Multiplatform 与 Android 场景与本文的规则文件构成互补它推荐kotlin.testKMP 通用与 JUnitAndroid 专属、用Turbine测Flow/StateFlowtest { 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.md80% 覆盖率、三类测试、TDD 强制循环规则层kotlin-testing.mdKotest spec 风格 MockK、runTest、koverHtmlReport/koverVerify能力层skills/kotlin-testing/SKILL.md四种 Spec 风格示例、matcher 与自定义 matcher、MockK 参数捕获与 Spy、Flow/虚拟时间测试、withData与属性化测试、Kover 完整 Gradle 配置、KtortestApplication、命令速查表与 CI 门禁。关注点落地方式依据规则按需生效frontmatterglobsalwaysApply: false.cursor/rules/kotlin-testing.md规则安装cursor组件写入./.cursor/scripts/install-apply.js框架选型KotestStringSpec/FunSpec/BehaviorSpec/DescribeSpec MockK.cursor/rules/kotlin-testing.md协程测试kotlinx-coroutines-test的runTest.cursor/rules/kotlin-testing.md、rules/kotlin/testing.md覆盖率门禁KoverkoverVerifyminBound(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),仅供参考