测试分层: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 的设计核心》

相关推荐
糯米导航4 小时前
AI 视觉回归实战:截图对比不是“找不同”,如何让智能差异分析真正服务 UI 质量
人工智能·ui·回归
-SOLO-5 小时前
Android Event 日志完全指南
android
壮哥_icon6 小时前
【Android 系统开发】从掉帧卡顿到丝滑:高频硬件中断(IRQ)的 CPU 亲和性性能优化实战
android·性能优化
千里马学框架6 小时前
Android Framework 新手学习踩坑建议指南
android·智能手机·性能优化·framework·aaos·车载开发·系统工程师
我命由我123458 小时前
执行 Gradle 指令报错,无法将“grep”项识别为 cmdlet、函数、脚本文件或可运行程序的名称
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
狂奔solar9 小时前
ARMA 模型:从白噪声到万能逼近器
android
渣渣灰飞9 小时前
MySQL 系统学习 第五阶段:企业级 MySQL 实战开发 第二章:RBAC 权限系统设计
android·学习·mysql
qq_422828629 小时前
android 图形学之图层数据送显及合成图层(七)
android
MiyamuraMiyako11 小时前
Compose:从自动滚动到双锚点 LazyLayout
android·android jetpack
CedarQR12 小时前
VS Code Copilot Chat 配置 MCP Server 全攻略(以 Next AI Draw.io 为例)
人工智能·ui·json