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.md、patterns.md、hooks.md、security.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)共享测试 | @Test、assertEquals、assertTrue |
| JUnit 4/5 | Android 专属单元测试 | @Test、@BeforeEach、断言 API |
| Turbine | Flow 与 StateFlow 的异步流断言 | test { }、awaitItem() |
| kotlinx-coroutines-test | 协程测试 | runTest、TestDispatcher、advanceUntilIdle() |
选择逻辑很清晰: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 强制工作流为:
- 先写测试(RED)
- 运行测试——应当失败
- 编写最小实现(GREEN)
- 运行测试——应当通过
- 重构(IMPROVE)
- 验证覆盖率(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),仅供参考