ECC 规则实践:Kotlin 与 Android/KMP 测试体系实战指南
2026/9/10 0:56:57 网站建设 项目流程

ECC 规则实践:Kotlin 与 Android/KMP 测试体系实战指南

【免费下载链接】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

本文基于 docs/ja-JP/rules/kotlin/testing.md(对应英文原版 rules/kotlin/testing.md)编写,并融合 ECC 仓库中的公共测试规则、Kotlin 架构模式与协程技能源码级佐证。

本文是 ECC(Agent Harness Performance Optimization System)为 Kotlin 及 Android/KMP 项目制定的测试规范实战指南。它继承自 ECC 公共测试规则 rules/common/testing.md,围绕「测试框架选型、ViewModel 与 Flow 测试、Fake 优先、协程测试、Ktor MockEngine、数据库测试、命名与目录组织」展开,并给出可直接复制运行的 Kotlin 代码示例。读完本文,你将掌握一套覆盖单元测试到 KMP 多平台测试的完整落地方案,并理解它与 TDD 工作流、tdd-guideAgent 之间的协作关系。

一、规则来源与适用场景

testing.md属于 ECC 仓库rules/kotlin/目录下的五份 Kotlin 专项规则之一(其余为coding-style.mdpatterns.mdhooks.mdsecurity.md)。其文件头通过paths字段声明适用范围:

--- paths: - "**/*.kt" - "**/*.kts" ---

也就是说,凡是仓库内新增或修改的 Kotlin 源文件(.kt)与构建脚本(.kts),都应当遵循该测试规范。文档开篇明确说明:「このファイルは common/testing.md を Kotlin および Android/KMP 固有のコンテンツで拡張します」(本文件以 Kotlin 及 Android/KMP 专属内容扩展公共测试规则)。公共规则位于 rules/common/testing.md,两者是「通用基线 + 语言专项」的叠加关系,下文会分别展开。

二、测试框架选型:四个层次的组合

规则规定了四种测试框架,各司其职:

框架用途关键 API
kotlin.test多平台(KMP)共享测试@TestassertEqualsassertTrue
JUnit 4/5Android 专属单元测试@Test@BeforeEach、断言 API
TurbineFlow 与 StateFlow 的异步流断言test { }awaitItem()
kotlinx-coroutines-test协程测试runTestTestDispatcheradvanceUntilIdle()

选择逻辑很清晰:KMP 的commonTest中依赖平台无关的kotlin.test;Android 平台测试使用 JUnit;凡是涉及Flow/StateFlow/SharedFlow的异步数据流,一律借助 Turbine 的test扩展逐帧断言;而所有涉及suspend函数的测试统一在runTest提供的虚拟时间环境中执行。这四者不是互斥选项,而是按被测对象的性质组合使用。

三、最低测试覆盖与 TDD 基线(继承自公共规则)

Kotlin 专项规则在文末给出硬性要求:所有功能(feature)必须至少覆盖 ViewModel + UseCase 两层测试(原文:「最低限のテストカバレッジ: すべての機能に対して ViewModel + UseCase」)。

这一要求是公共测试规则在 Kotlin 层的落地。ECC 的 rules/common/testing.md 规定整体覆盖率基线为80%,并明确三类测试缺一不可:单元测试(函数/工具/组件)、集成测试(API 端点、数据库操作)、端到端测试(关键用户流程)。其配套的TDD 强制工作流为:

  1. 先写测试(RED)
  2. 运行测试——应当失败
  3. 编写最小实现(GREEN)
  4. 运行测试——应当通过
  5. 重构(IMPROVE)
  6. 验证覆盖率(80%+)

同时建议在测试失败时按序排查:使用tdd-guideAgent → 检查测试隔离性 → 核对 mock 是否正确 → 修复实现而非测试(除非测试本身有误)。公共规则还推荐测试内部采用AAA 三段式结构(Arrange-Act-Assert),便于读者快速定位「准备数据 / 触发动作 / 校验结果」三个环节。

四、ViewModel 测试:Turbine 逐帧断言状态流

Kotlin 专项规则给出第一个核心示例——用 Turbine 测试以StateFlow驱动的 ViewModel:

@Test fun `loading state emitted then data`() = runTest { val repo = FakeItemRepository() repo.addItem(testItem) val viewModel = ItemListViewModel(GetItemsUseCase(repo)) viewModel.state.test { assertEquals(ItemListState(), awaitItem()) // 初始状态 viewModel.onEvent(ItemListEvent.Load) assertTrue(awaitItem().isLoading) // 加载中 assertEquals(listOf(testItem), awaitItem().items) // 加载完成 } }

这段测试之所以成立,前提是被测 ViewModel 遵循了 rules/kotlin/patterns.md 定义的ViewModel 模式:单一不可变状态对象(data class ScreenState)、事件入口(onEvent)、单向数据流(内部MutableStateFlow暴露为asStateFlow())。测试逐一awaitItem(),把「初始态 → 加载中 → 加载完成」三个状态按顺序断言,恰好验证了单向数据流的状态迁移时序。

更复杂的多 Flow 组合场景(如combine多个数据源)同样可以用 Turbine 断言最终合并结果,详见 skills/kotlin-coroutines-flows/SKILL.md 中「Testing StateFlow with Turbine」一节——那里展示了onSearch("query")触发后依次断言 loading 态与 loaded 态的完整写法。

五、Fake 优先于 Mock:手写替身的正确姿势

规则明确:优先使用手写 Fake,而非 Mock 框架(原文:「モッキングフレームワークよりも手書きのフェイクを優先する」)。示例:

class FakeItemRepository : ItemRepository { private val items = mutableListOf<Item>() var fetchError: Throwable? = null override suspend fun getAll(): Result<List<Item>> { fetchError?.let { return Result.failure(it) } return Result.success(items.toList()) } override fun observeAll(): Flow<List<Item>> = flowOf(items.toList()) fun addItem(item: Item) { items.add(item) } }

这个 Fake 与 rules/kotlin/patterns.md 中定义的ItemRepository接口严格对应:suspend函数返回Result<T>(可用fetchError注入失败路径),Flow承载响应式数据流。Fake 的价值在于:它是一段真实的、可读的实现,能随接口演化同步修改,行为完全由测试代码掌控,不会出现 Mock 框架常见的「过度打桩导致测试与实现脱钩」问题。在 skills/kotlin-coroutines-flows/SKILL.md 的「Faking Flows」一节,还给出了基于MutableStateFlow的可发射型 Fake——通过emit(items)主动推送数据,配合 Turbine 断言流式更新。

六、协程测试:runTest 与虚拟时间

规则指出,所有协程相关测试都应使用runTest

@Test fun `parallel operations complete`() = runTest { val repo = FakeRepository() val result = loadDashboard(repo) advanceUntilIdle() assertNotNull(result.items) assertNotNull(result.stats) }

runTest的核心能力是:自动推进虚拟时间并提供TestScope(原文:「仮想時間を自動的に進め、TestScopeを提供する」)。这意味着测试中的delay(...)不再真实等待,而是被虚拟时钟瞬间跳过,测试速度大幅提升且确定性极高。advanceUntilIdle()用于把虚拟时钟推进到所有待执行协程(包括async并行任务)全部完成——这正是上面loadDashboard这类「并行加载仪表盘」场景的关键。

需要补充的一点:runTest默认使用StandardTestDispatcher,被测 ViewModel 内部viewModelScope.launch派发的任务同样在该测试调度器上排队,因此测试是确定的;若测试中有自定义的Dispatchers注入点,可在测试中替换为UnconfinedTestDispatcher或标准测试调度器,以保证时序可控。kotlinx-coroutines-test提供的TestDispatcher体系(StandardTestDispatcher/UnconfinedTestDispatcher/TestScope)是这条规则的底层支撑。

七、网络层测试:Ktor MockEngine

针对基于 Ktor 的 HTTP 客户端,规则推荐使用MockEngine拦截请求并返回预设响应:

val mockEngine = MockEngine { request -> when (request.url.encodedPath) { "/api/items" -> respond( content = Json.encodeToString(testItems), headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()) ) else -> respondError(HttpStatusCode.NotFound) } } val client = HttpClient(mockEngine) { install(ContentNegotiation) { json() } }

要点拆解:

  • MockEngine接收一个以HttpRequestData为入参、返回HttpResponseData的 lambda,通过request.url.encodedPath基于路径的路由
  • 命中路径返回 JSON 序列化内容,并显式设置Content-Type: application/json响应头——这保证了ContentNegotiation的 JSON 反序列化链路在测试中真实生效;
  • 未命中路径返回404,可用来断言客户端对错误状态码的处理(如抛出自定义异常或降级逻辑)。

通过为同一个HttpClient注入不同的HttpClientEngine,即可在「测试环境用MockEngine、生产环境用真实引擎(如OkHttp/CIO/Darwin)」之间无缝切换,无需改动上层 Repository 的调用代码。

八、数据库层测试:Room 与 SQLDelight

规则给出两类数据库的测试策略:

  • Room:使用Room.inMemoryDatabaseBuilder()构建内存数据库,测试结束后自动销毁,无需清理文件;
  • SQLDelight:JVM 测试使用JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)内存驱动。

SQLDelight 示例:

@Test fun `insert and query items`() = runTest { val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) Database.Schema.create(driver) val db = Database(driver) db.itemQueries.insert("1", "Sample Item", "description") val items = db.itemQueries.getAll().executeAsList() assertEquals(1, items.size) }

这里体现了 SQLDelight 的典型流程:先用Database.Schema.create(driver)在内存驱动上建表,再创建Database实例,随后直接调用编译期生成的类型安全查询itemQueries。由于驱动是纯内存的,每个测试方法天然隔离,配合runTest即可稳定执行。规则没有展开的是:若测试涉及异步 Flow 式查询(Flow返回值),同样可用 Turbine 对数据库变更流做断言;而 Android 平台上的插桩测试(androidInstrumentedTest)则可使用Room.inMemoryDatabaseBuilder(context, ...)验证真实 SQLite 行为。

九、测试命名:反引号内的行为描述

规则要求使用反引号包裹的描述性命名,把测试方法名写成一句可读的英文行为描述:

@Test fun `search with empty query returns all items`() = runTest { } @Test fun `delete item emits updated list without deleted item`() = runTest { }

这种命名与 rules/common/testing.md 中「使用能解释被测行为的描述性名称」一脉相承(公共规则中的示例为returns empty array when no markets match query等)。其好处是:测试失败时,失败信息本身就是一句可读的需求陈述(如「search with empty query returns all items」),无需跳回代码解读;同时测试名可反推被测行为,天然充当可执行的需求文档。

十、KMP 测试目录组织

规则给出多平台项目的标准测试目录结构:

src/ ├── commonTest/kotlin/ # 共享测试(ViewModel、UseCase、Repository) ├── androidUnitTest/kotlin/ # Android 单元测试(JUnit) ├── androidInstrumentedTest/kotlin/ # 插桩测试(Room、UI) └── iosTest/kotlin/ # iOS 专属测试

分层逻辑与 Kotlin Multiplatform 的src源集结构一一对应:

  • commonTest:放置与平台无关的共享逻辑测试——ViewModel、UseCase、Repository 的测试都应在此,这也呼应了「每个功能至少覆盖 ViewModel + UseCase」的最低要求;
  • androidUnitTest:依赖 Android 框架(JVM 可模拟部分)的单元测试,使用 JUnit 4/5;
  • androidInstrumentedTest:需要真机/模拟器运行、访问 Room 数据库或 UI 的插桩测试;
  • iosTest:iOS 平台专属逻辑(如expect/actual的 iOS 实现)测试。

十一、与 TDD 工作流和 Agent 的协作

Kotlin 测试规则并非孤立存在,它嵌在 ECC 更宏观的开发流程中:

  • TDD 流程:公共规则强制「先写测试(RED)→ 确认失败 → 最小实现(GREEN)→ 重构(IMPROVE)→ 验证 80%+ 覆盖率」。Kotlin 专项规则提供的就是 RED 阶段「怎么写」的具体武器——Turbine 写 ViewModel 测试、Fake 替身、runTest虚拟时间、MockEngine打桩网络层。
  • tdd-guideAgent:公共规则与 agents/tdd-guide.md 均指明,开发新功能时应主动激活tdd-guideAgent。该 Agent 的角色定义是「Test-Driven Development specialist enforcing write-tests-first methodology」,负责强制「先测试后代码」、引导 Red-Green-Refactor 循环、确保 80%+ 覆盖率并编写单元/集成/E2E 测试套件——Kotlin 专项规则是它在 Kotlin/Android/KMP 项目中的具体检查清单。

十二、常见反模式与陷阱

结合 skills/kotlin-coroutines-flows/SKILL.md 的「Anti-Patterns to Avoid」清单,与本文规则相关的常见陷阱包括:

  • 使用GlobalScope:协程泄漏、无结构化取消,测试中也无法由TestScope统一控制,应一律使用viewModelScope等生命周期作用域;
  • init {}中无作用域收集 Flow:应改为viewModelScope.launch
  • 对可变集合使用MutableStateFlow:状态更新必须走不可变副本,如_state.update { it.copy(list = it.list + newItem) },否则状态流无法正确触发重组与断言;
  • 捕获CancellationException:应让其继续传播以保证取消语义,测试中吞掉取消异常会导致runTest超时或悬挂;
  • @Composable中不借助remember创建 Flow:每次重组都重建流,导致 Turbine 断言到的流实例与 UI 实际消费的不一致。

十三、小结

ECC 的 Kotlin 测试规则提供了一条完整的实践路径:以kotlin.test+ JUnit 打底,用 Turbine 精确断言 Flow/StateFlow 的发射时序,用runTest掌控虚拟时间,用 Fake 替身替代 Mock 框架,用 KtorMockEngine隔离网络层,用内存驱动测试 Room/SQLDelight 数据库,再以行为化命名与 KMP 源集目录组织保证可读性与可维护性。它与公共 80% 覆盖率基线、TDD 强制流程及tdd-guideAgent 共同构成「规范可查、示例可跑、工具可循」的 Kotlin 质量保障闭环。

需要深入时,可继续阅读本仓库中的相关文档:rules/common/testing.md(公共测试基线)、rules/kotlin/patterns.md(ViewModel/UseCase/Repository 模式,本文示例的架构前提)、skills/kotlin-coroutines-flows/SKILL.md(协程与 Flow 测试进阶与反模式)、agents/tdd-guide.md(TDD 强制流程 Agent)。

【免费下载链接】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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询