测试分层:JVM 单测、ViewModel 测试与 Compose UI Test

技术目标

  1. 会写 无 Android 框架依赖 的纯 Kotlin 单测(本仓库 reduceStateSample)。
  2. 知道 Compose UI Test 需要额外依赖与 device/emulator 环境,入口 API 是 createComposeRule()setContentonNodeWithText 等。
  3. 能区分 PR 上跑什么、本地/nightly 跑什么,避免把 flaky instrumented 全堆在 merge 门禁上。

1. 测试分层(心智图)

慢 / device
Compose UI Test
端到端 / 少量冒烟
中速
ViewModel 逻辑 + TestScope
快 / JVM
Reducer 纯函数
UseCase / Parser

  • 纯函数与 reducer :最快、最稳定,应覆盖 状态迁移 + 分支 + Effect 是否出现
  • ViewModel :可测 viewModelScopeStateFlow 的 wiring(需 MainDispatcherRule 等);本仓库当前以 reducer 为主力。
  • Compose UI Test / 真机 :验证 交互、语义、导航 ;注意 动画、时间、Idle 导致的 flaky。

2. JVM 单测:StateSampleReducerTest

断言要点(与测试代码一致):

用例 断言
Increment count 自增; Effect
Save lastSavedSummary 含当前 count;EffectShowSavedSnackbar 且 message 与 summary 一致

运行(模块级):

bash 复制代码
./gradlew :app:testDebugUnitTest --tests "com.kuen.composedemo.samples.state.StateSampleReducerTest"

运行 app 模块全部 JVM 单测:

bash 复制代码
./gradlew :app:testDebugUnitTest

局限 :不覆盖 StateSampleViewModelviewModelScope.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 级单测。
  • FlakyDispatchers.Main 未替换、全局 timer、随机数据 → CI 随机红。
  • 只追覆盖率:无有意义断言的分支 = 白测。
  • 在 Android Test 里跑真实网络/真实后端:应 mock 或 fake;否则慢且不稳定。

6. 本篇在系列中的位置

前 07 篇主要讲 Compose 写法与运行机制;从本文开始,把重点转向 如何验证这些写法是否可靠。建议把 reducer / use case 等纯逻辑优先沉到 JVM 单测,把少量真正依赖设备语义、导航或交互的场景放到 Compose UI Test / instrumented 测试。


7. 工程化(收束)

团队规模化时,在 API 之上补:lint 规则、模块化边界、Review 清单,才能把「会写测试」变成「CI 可信」。


8. 自检清单

  1. 新业务状态迁移是否 落在可 JVM 测的纯函数上?
  2. PR 门禁是否以 JVM 单测 为主、instrumented 为辅?
  3. UI Test 是否避免 依赖真实时间、真实网络、未关闭动画
  4. 失败用例是否有 稳定复现步骤断言信息(而非仅截图)?

参考答案(复习用)

  1. 建议先 。如 reduceStateSample:纯函数 + StateSampleReducerTest,分支与 Effect 一眼断言;ViewModel 只做薄胶水时再补 VM 测。
  2. 建议如此 。merge 前 ./gradlew :app:testDebugUnitTest 快且稳;connectedCheck 慢、易 flaky,适合 nightly 或关键路径少量条数。
  3. 应避免 。用 TestDispatcher、Fake 仓库、waitUntil;动画可用测试规则关闭或等待 idle,减少随机红。
  4. 应有 。CI 日志里能看到 assertEquals 期望值与实际值;本地复现命令写进 PR 描述,避免「只红截图无法修」。

源码仓库ComposeDemo(分支 main

系列推荐

《Navigation Compose:NavHost、NavController 与参数》

《深入 MaterialTheme:掌握 ColorScheme 与 Typography 的设计核心》

相关推荐
m0_463672201 小时前
如何优雅处理SQL存储过程异常_使用TRY-CATCH块机制
jvm·数据库·python
背包客(wyq)1 小时前
开源中文语音模型Android端部署测试
android·开源
yewq-cn1 小时前
Android Compatibility
android
早起傻一天~G1 小时前
vue2+element-UI上传文件
javascript·vue.js·ui
ㄟ留恋さ寂寞1 小时前
HTML5中SharedWorker生命周期与浏览器进程关闭的关系
jvm·数据库·python
彳亍1011 小时前
MongoDB备节点无法读取数据怎么解决_rs.slaveOk()与Secondary读取权限
jvm·数据库·python
m0_690825821 小时前
CSS如何实现圆形头像裁剪_使用border-radius50属性
jvm·数据库·python
老纪1 小时前
HTML函数工具在NAS设备上能运行吗_轻服务器适配指南【指南】
jvm·数据库·python
老纪1 小时前
SQL如何高效提取大表前几行:分页查询与OFFSET优化
jvm·数据库·python