ComposeTestRule详解:createComposeRule与其他规则的区别,作用域与生命周期
Compose 测试的核心入口就是 ComposeTestRule。说实话,很多新手一上来就闷头写测试,结果发现怎么跑都是 NoSuchElement 或者 Activity 没创建。嗯,根本原因就是对几种 Rule 的职责边界没摸清楚。
我带的团队里,新同学至少会在这上面栽一两次跟头。所以这一章我们掰开揉碎讲清楚。
三种 Rule 是什么?各自什么时候用?
官方提供了三个常用的 TestRule 实现,来看对照表——
| Rule 名称 | 适用场景 | 是否启动 Activity | 依赖 |
|---|---|---|---|
createComposeRule | 纯 Compose 组件测试 | 否(仅创建 Composition) | 无 Activity 上下文 |
createAndroidComposeRule | 需要 Activity 生命周期的集成测试 | 是(自动启动 Activity) | ActivityScenario |
createEmptyComposeRule | 对已有 Activity 手动注入 ComposeContent | 需外部配合(如 ActivityScenario.launch) | 手动管理 Composition |
我个人的习惯是:能不用 Activity 就别用。纯 UI 组件用 createComposeRule 速度快得多。只有在需要测试 onCreate / onResume 触发状态、或者混合 View 系统时才用 createAndroidComposeRule。
关键认知:createComposeRule 底层是一个 ComposeTestRule 的实现,它只有 Composition 域,没有 Activity 的 Context 和 Lifecycle。说白了,它就是个“轻量级沙盒”。
作用域与生命周期——别在错误的地方找 View
很多测试失败都跟作用域有关。你想想看——
createComposeRule的作用域:仅在setContent内。一旦测试方法结束,Composition 就销毁了。createAndroidComposeRule的作用域:延伸至 Activity 的生命周期。你可以在@Test中拿到 Activity 引用,甚至模拟屏幕旋转。createEmptyComposeRule最灵活,也最容易犯错——因为它只提供一个空的ComposeTestRule,你必须自己管理setContent的时机。
我曾经犯过一个错误:用 createComposeRule 测试一个依赖 Context 获取资源的 composable,结果 LocalContext.current 一直为 null。后来才意识到——纯 ComposeRule 根本没有 Context 注入。解决方法是要么传参数,要么改用 createAndroidComposeRule。
避坑指南:createComposeRule 下不能使用 ActivityScenarioRule 的 ActivityResult 或者 onActivity 方法。如果你在测试中直接调了 rule.activity,那就等着 crash 吧。
生命周期的差异如何影响测试行为
我们写几个对比 Demo 看看——
// 场景1:纯 ComposeRule —— 只有 Composition 域
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun testPureCompose() {
composeTestRule.setContent {
MyButton(text = "点击")
}
composeTestRule.onNodeWithText("点击").assertIsDisplayed()
}
// 场景2:AndroidComposeRule —— Activity 生命周期参与
@get:Rule
val androidComposeRule = createAndroidComposeRule(TestActivity::class.java)
@Test
fun testWithActivityLifecycle() {
androidComposeRule.onNodeWithText("点击").assertDoesNotExist()
// 此时 Activity 已经 onCreate 完成,但 setContent 还未调用
androidComposeRule.activity.setContentView(...) // 你甚至可以混用 View 系统
}
// 场景3:EmptyComposeRule —— 完全手动
@get:Rule
val emptyRule = createEmptyComposeRule()
@Test
fun testManualSetContent() {
emptyRule.setContent {
// 需要自己管理 composition
}
}
核心生命周期节点对比
| 阶段 | createComposeRule | createAndroidComposeRule | createEmptyComposeRule |
|---|---|---|---|
| @Before | Rule 可用,无 Content | Activity 已创建,setContent 未执行 | Rule 可用,无 Content |
| setContent 后 | Composition 进入 Composition | Composition 进入 Composition | Composition 进入 Composition |
| @Test 中 | 可交互,但无 Activity 引用 | 可交互,可通过 rule.activity 获取 Activity | 可交互,无 Activity 引用 |
| @After | Composition 自动销毁 | Activity 自动 Finish | Composition 自动销毁(前提是你调过 setContent) |
我的小技巧:如果你写一个自定义 Rule 组合了 ComposeTestRule 和 ActivityScenarioRule,一定要保证 @Rule 的顺序——通常把 ActivityScenarioRule 放在 ComposeTestRule 前面,否则 createAndroidComposeRule 帮你做的那些魔法可能会失效。
一张图看懂三种 Rule 的包裹关系
下面这张 SVG 帮您理清作用域和生命周期的层级:
实战中如何选择?我总结了三句话
- 只测 Compose 组件行为 —— 无脑用
createComposeRule。速度最快,也不依赖 instrumentation 环境。 - 要测页面跳转、权限弹窗、SharedPreferences 等系统交互 —— 上
createAndroidComposeRule,配合ActivityScenario还能模拟旋转。 - 你需要一个干净的 Rule 来组装自己的复合规则 ——
createEmptyComposeRule就是你的乐高底板。但记得手动setContent,不然什么都找不到。
我记得有一次重构一个复杂页面,里面有大量 LaunchedEffect 和 DisposableEffect。如果用 createComposeRule,生命周期只走一次 LaunchedEffect,根本测不到 onDispose 的回调。后来换成 createAndroidComposeRule,配合 scenario.recreate() 才能完整覆盖。所以,别贪图方便选错了武器。
一句话总结:createComposeRule 是“沙盒”,createAndroidComposeRule 是“全量Activity”,createEmptyComposeRule 是“半成品”。选哪个取决于你需要多大的生命周期上下文。