第4章 Compose测试规则详解:createComposeRule vs createAndroidComposeRule、Activity设置、自定义Content
4.1 概念与作用域
Compose UI测试的核心入口是ComposeTestRule接口,它负责管理Composable的合成生命周期、调度帧、等待空闲状态以及提供语义树查询。Jetpack Compose测试库提供了两个具体实现类:
- createComposeRule() — 轻量级规则,仅启动Compose环境,不依赖Activity生命周期;适用于纯组件级测试。
- createAndroidComposeRule<Activity>() — 全栈规则,启动一个真实Activity并嵌入ComposeContent;可用于需要Activity生命周期、Context、资源、Intent的集成测试。
核心原则: 组件级测试首选
createComposeRule,集成测试(需Activity、Fragment、导航)必须使用createAndroidComposeRule。
4.2 规则选择对比表
| 维度 | createComposeRule | createAndroidComposeRule<A> |
|---|---|---|
| 启动速度 | 快(无Activity创建) | 中等(需启动Activity) |
| Activity生命周期 | 无 | 完整(onCreate、onStart等) |
| Context / 资源 | 仅Compose环境 | 真实Activity Context |
| Intent / 权限 | 不支持 | 支持(需Instrumentation) |
| Fragment / Navigation | 需额外模拟 | 原生支持 |
| 适用场景 | 单元测试、组件快照 | UI集成测试、端到端 |
4.3 Activity设置详解
当使用createAndroidComposeRule时,可以通过泛型参数指定Activity类型。默认启动ComponentActivity,也可指定自定义Activity:
@Rule
@JvmField
val composeTestRule = createAndroidComposeRule<MainActivity>()
若需要控制Activity的Intent或启动参数,使用launchActivity方法:
composeTestRule.activityScenario.launch(
Intent(ApplicationProvider.getApplicationContext(), MainActivity::class.java).apply {
putExtra("test_key", "test_value")
}
)
4.3.1 Activity设置流程
- 规则初始化时通过
ActivityScenario创建Activity实例。 - Activity的
onCreate中通常调用setContent,该Content由规则内部维护。 - 可通过
composeTestRule.activity获取当前Activity引用,用于断言或触发系统行为。
避坑:
createAndroidComposeRule中的Activity只能启动一次,若测试中需要重新创建Activity(如旋转屏幕),需使用ActivityScenario.recreate(),并在composeTestRule.waitForIdle()后继续断言。
4.4 自定义Content:setContent与自定义Composable
无论是哪种规则,都需要通过setContent加载被测Composable。自定义Content通常指在测试中组装特定的UI层次,包括:
- 提供主题(MaterialTheme、自定义Theme)
- 注入依赖(ViewModel、Repository)
- 包裹容器(Box、Scaffold、NavHost)
- 设置状态(remember、mutableStateOf)
4.4.1 基础用法
composeTestRule.setContent {
// 自定义Content
MyButton(
text = "Submit",
onClick = { /* 测试逻辑 */ }
)
}
4.4.2 带主题与依赖注入的常用模式
@Test
fun testButtonWithTheme() {
composeTestRule.setContent {
AppTheme(darkTheme = false) {
Surface {
val viewModel = remember { FakeViewModel() }
MyButton(viewModel = viewModel)
}
}
}
// 断言...
}
4.4.3 自定义Content的职责边界
| 职责 | 示例 | 说明 |
|---|---|---|
| 环境搭建 | 主题、Surface、Box | 确保Composable在真实环境中渲染 |
| 数据层模拟 | FakeViewModel、TestRepository | 隔离外部依赖,控制测试数据 |
| 嵌套组件 | NavHost、Scaffold | 测试导航或脚手架场景 |
| 状态容器 | mutableStateOf、StateFlow | 模拟用户交互导致的状态变化 |
实践建议: 将自定义Content提取为独立的
@Composable函数(如TestContent),方便复用并减少测试代码冗余。
4.5 规则初始化流程与生命周期
4.6 实现流程:从规则声明到断言
- 声明规则 — 使用
@get:Rule注解(JUnit4)或@TestRule(JUnit5)。 - 设置Content — 在
@Before或每个测试方法中调用setContent,传入自定义Composable。 - 触发行为 — 使用
onNodeWithText()、performClick()等语义操作。 - 等待空闲 —
waitForIdle()或awaitIdle()确保Compose动画与重组完成。 - 断言结果 — 通过
assertIsDisplayed()、assertTextEquals()或自定义断言。
@get:Rule
val rule = createComposeRule()
@Test
fun customContentTest() {
rule.setContent {
val count = remember { mutableStateOf(0) }
Button(onClick = { count.value++ }) {
Text("Click $count")
}
}
rule.onNodeWithText("Click 0").assertExists()
rule.onNodeWithTag("button").performClick()
rule.waitForIdle()
rule.onNodeWithText("Click 1").assertExists()
}
4.7 常见错误与调试
- “No compose nodes found” — Content未正确set或使用了错误的
onNode匹配器。先通过printToLog()打印完整语义树。 - “Activity destroyed” — 在异步回调中访问
activity;使用activityScenario.onActivity { }确保在UI线程访问。 - “Compose content is not set” — 忘记调用
setContent;检查@Before或测试方法体内的调用顺序。 - “Multiple rules” — 声明了多个
@Rule未按顺序执行;使用RuleChain控制执行顺序。
4.8 性能与稳定性优化
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 复用规则实例 | 一个@ClassRule共享一个规则,减少重复启动 | 多个测试共享相同Content |
使用waitForIdle超时 | 避免测试因动画卡死;设置timeoutMillis | 含动画或异步网络 |
| 避免真实依赖 | 用Fake/Test双代替Repository,减少I/O | 单元测试场景 |
| 减少Content重建 | 在@Before中统一setContent,不要在每条测试中重复 | 批量常规测试 |
关键总结:
createComposeRule适用于纯Composable单元测试,最快最轻。createAndroidComposeRule适用于集成测试,需Activity参与。- 自定义Content是测试的“舞台”,通过主题、依赖、容器构建真实测试环境。
- 善用语义树打印与超时控制,提升调试效率和稳定性。