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
    }
}

核心生命周期节点对比

阶段createComposeRulecreateAndroidComposeRulecreateEmptyComposeRule
@BeforeRule 可用,无 ContentActivity 已创建,setContent 未执行Rule 可用,无 Content
setContent 后Composition 进入 CompositionComposition 进入 CompositionComposition 进入 Composition
@Test 中可交互,但无 Activity 引用可交互,可通过 rule.activity 获取 Activity可交互,无 Activity 引用
@AfterComposition 自动销毁Activity 自动 FinishComposition 自动销毁(前提是你调过 setContent)

我的小技巧:如果你写一个自定义 Rule 组合了 ComposeTestRule 和 ActivityScenarioRule,一定要保证 @Rule 的顺序——通常把 ActivityScenarioRule 放在 ComposeTestRule 前面,否则 createAndroidComposeRule 帮你做的那些魔法可能会失效。

一张图看懂三种 Rule 的包裹关系

下面这张 SVG 帮您理清作用域和生命周期的层级:

ComposeTestRule 三种规则的层级与生命周期 @Test 作用域 createComposeRule 纯 Compose 沙盒 无 Activity / 无 Context setContent { ... } 仅 Composition 域 ⚠ 注意 不能使用 activity LocalContext 需要手动提供 createAndroidComposeRule 集成 Activity 生命周期 ActivityScenario onCreate / onResume / onPause setContent 在内部执行 可通过 rule.activity 访问 扩展 createEmptyComposeRule:纯空壳,无 Activity 也无默认 setContent,完全手动控制

实战中如何选择?我总结了三句话

  1. 只测 Compose 组件行为 —— 无脑用 createComposeRule。速度最快,也不依赖 instrumentation 环境。
  2. 要测页面跳转、权限弹窗、SharedPreferences 等系统交互 —— 上 createAndroidComposeRule,配合 ActivityScenario 还能模拟旋转。
  3. 你需要一个干净的 Rule 来组装自己的复合规则 —— createEmptyComposeRule 就是你的乐高底板。但记得手动 setContent,不然什么都找不到。

我记得有一次重构一个复杂页面,里面有大量 LaunchedEffect 和 DisposableEffect。如果用 createComposeRule,生命周期只走一次 LaunchedEffect,根本测不到 onDispose 的回调。后来换成 createAndroidComposeRule,配合 scenario.recreate() 才能完整覆盖。所以,别贪图方便选错了武器。

一句话总结:createComposeRule 是“沙盒”,createAndroidComposeRule 是“全量Activity”,createEmptyComposeRule 是“半成品”。选哪个取决于你需要多大的生命周期上下文。