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

相关推荐
方白羽5 小时前
Android Gradle 缓存与文件目录深度解析
android·gradle·android studio
曲幽8 小时前
Termux里的二进制和脚本,到底怎么运行才不踩坑?Termux-service 保活妙招!
android·termux·nohup·services·wake-lock
plainGeekDev9 小时前
单例模式 → object 声明
android·java·kotlin
程序员陆业聪9 小时前
读者点单·03|Compose 与传统 View 混用的 12 个真实坑
android
程序员陆业聪10 小时前
读者点单·02|Android 启动优化实战:Trace 抓取→Application 编排→冷启动全流程拆解
android
Coffeeee10 小时前
帮你快速理解AI Agent之我想招个Android实习生
android·人工智能·agent
恋猫de小郭11 小时前
苹果 AirPods 协议,Android 也可以使用完整版 AirPods 能力
android·前端·flutter
黄林晴11 小时前
告别无效重建:Gradle 9.6.0 解决 CI 构建缓存失效痛点告别无效重建:Gradle 9.6.0 解决 CI 建筑缓存失效痛点
android·gradle
张风捷特烈12 小时前
Flutter 类库大揭秘#01 | path_provider架构与设计
android·flutter
_阿南_21 小时前
Android文件读写和分享总结
android