技术目标
- 会写 无 Android 框架依赖 的纯 Kotlin 单测(本仓库
reduceStateSample)。 - 知道 Compose UI Test 需要额外依赖与 device/emulator 环境,入口 API 是
createComposeRule()、setContent、onNodeWithText等。 - 能区分 PR 上跑什么、本地/nightly 跑什么,避免把 flaky instrumented 全堆在 merge 门禁上。
1. 测试分层(心智图)
慢 / device
Compose UI Test
端到端 / 少量冒烟
中速
ViewModel 逻辑 + TestScope
快 / JVM
Reducer 纯函数
UseCase / Parser
- 纯函数与 reducer :最快、最稳定,应覆盖 状态迁移 + 分支 + Effect 是否出现。
- ViewModel :可测
viewModelScope与StateFlow的 wiring(需MainDispatcherRule等);本仓库当前以 reducer 为主力。 - Compose UI Test / 真机 :验证 交互、语义、导航 ;注意 动画、时间、Idle 导致的 flaky。
2. JVM 单测:StateSampleReducerTest
- 被测代码:
StateSampleReducer.kt中reduceStateSample。 - 测试文件:
StateSampleReducerTest.kt。
断言要点(与测试代码一致):
| 用例 | 断言 |
|---|---|
Increment |
count 自增;无 Effect |
Save |
lastSavedSummary 含当前 count;Effect 为 ShowSavedSnackbar 且 message 与 summary 一致 |
运行(模块级):
bash
./gradlew :app:testDebugUnitTest --tests "com.kuen.composedemo.samples.state.StateSampleReducerTest"
运行 app 模块全部 JVM 单测:
bash
./gradlew :app:testDebugUnitTest
局限 :不覆盖 StateSampleViewModel 里 viewModelScope.launch { _effects.send(...) } 的并发与取消;关键路径可补 轻量 ViewModel 测试 或 少量 instrumented 冒烟。
3. Instrumented 现状(本仓库)
ExampleInstrumentedTest.kt 为模板级 useAppContext 断言包名;可作为 device 流水线是否接通 的探针。真正的 Compose UI Test 需在此基础上增加 createComposeRule()、setContent { ... }、onNodeWith... 等(见官方 Compose 测试)。
运行 instrumented 测试(需要已连接设备或 emulator):
bash
./gradlew :app:connectedDebugAndroidTest
4. Compose UI Test(概念与最小模板)
当前仓库还没有真正接入 Compose UI Test 依赖;app/build.gradle.kts 里已有 Compose BOM、AndroidX JUnit、Espresso,但还需补:
kotlin
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.test.manifest)
对应 version catalog 可加入:
toml
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
最小测试类骨架:
kotlin
import androidx.compose.material3.Text
import androidx.compose.ui.test.assertExists
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import org.junit.Rule
import org.junit.Test
class GreetingComposeTest {
@get:Rule
val rule = createComposeRule()
@Test
fun showTitle() {
rule.setContent { Text("Hi") }
rule.onNodeWithText("Hi").assertExists()
}
}
如果测试真实主题或组件,再在 setContent 中包项目自己的 Theme:
kotlin
@get:Rule
val rule = createComposeRule()
@Test
fun showTitle() {
rule.setContent { AppTheme { Text("Hi") } }
rule.onNodeWithText("Hi").assertExists()
}
注意:
- 异步列表:
waitUntil { ... }、或注册 IdlingResource。 - 导航 :常用
TestNavHostController+ 单独NavHost测试模块;参见 Navigation 官方测试文档。 - 动画:测试可关闭动画或等待 idle,减少 flaky。
5. 风险清单
- Robolectric 替代 JVM 全家桶 :维护与行为差异成本高,适合 补充 而非替代 reducer 级单测。
- Flaky :
Dispatchers.Main未替换、全局 timer、随机数据 → CI 随机红。 - 只追覆盖率:无有意义断言的分支 = 白测。
- 在 Android Test 里跑真实网络/真实后端:应 mock 或 fake;否则慢且不稳定。
6. 本篇在系列中的位置
前 07 篇主要讲 Compose 写法与运行机制;从本文开始,把重点转向 如何验证这些写法是否可靠。建议把 reducer / use case 等纯逻辑优先沉到 JVM 单测,把少量真正依赖设备语义、导航或交互的场景放到 Compose UI Test / instrumented 测试。
7. 工程化(收束)
团队规模化时,在 API 之上补:lint 规则、模块化边界、Review 清单,才能把「会写测试」变成「CI 可信」。
8. 自检清单
- 新业务状态迁移是否 先 落在可 JVM 测的纯函数上?
- PR 门禁是否以 JVM 单测 为主、instrumented 为辅?
- UI Test 是否避免 依赖真实时间、真实网络、未关闭动画?
- 失败用例是否有 稳定复现步骤 与 断言信息(而非仅截图)?
参考答案(复习用)
- 建议先 。如
reduceStateSample:纯函数 +StateSampleReducerTest,分支与 Effect 一眼断言;ViewModel 只做薄胶水时再补 VM 测。 - 建议如此 。merge 前
./gradlew :app:testDebugUnitTest快且稳;connectedCheck慢、易 flaky,适合 nightly 或关键路径少量条数。 - 应避免 。用
TestDispatcher、Fake 仓库、waitUntil;动画可用测试规则关闭或等待 idle,减少随机红。 - 应有 。CI 日志里能看到
assertEquals期望值与实际值;本地复现命令写进 PR 描述,避免「只红截图无法修」。
源码仓库 :ComposeDemo(分支
main)