ComposeTestRule 核心概念与生命周期:createComposeRule、AndroidComposeTestRule、测试规则的生命周期绑定

1 原理:测试规则的本质与设计意图

ComposeTestRule 是 Compose UI 测试框架中用于控制 Compose 内容渲染、执行异步等待和触发断言的统一入口。它封装了 Activity 或 Compose 宿主容器的生命周期,使得测试代码不需要手动管理界面线程调度。从实现角度看,测试规则是一个 JUnit 的 TestRule,它拦截测试方法的执行,在方法前后自动完成宿主环境(Activity 或 Compose 虚拟宿主)的启动与销毁。

Compose 官方提供了两条实践路径:

测试规则类型适用场景依赖 Android 环境宿主载体
createComposeRule()纯 Compose 组件单元测试否(可在 Robolectric/JVM 运行)虚拟宿主(ComposeScene)
AndroidComposeTestRule集成测试(需真实 Activity)是(Instrumentation)真实 Activity

两条规则共享相同的 ComposeTestRule 接口,因此 setContent、waitForIdle、onNodeWithText 等 API 签名完全一致。这也是“测试夹具隔离”的基础——测试代码不需要关心底层是纯 Compose 还是 Android 环境。

2 核心 API 与生命周期绑定

理解生命周期绑定,必须掌握测试规则在 JUnit 执行流中的“钩子”位置:

  1. @Before 方法执行之前:规则启动宿主(Activity 或 ComposeScene),完成组件树初始化。
  2. @Before 方法执行:用户可在此处注入 Mock、设置全局状态。
  3. 测试方法体 @Test:使用 setContent 加载 Composable,执行交互与断言。
  4. @After 方法执行:用户清理 Mock 或资源。
  5. @After 方法之后:规则销毁宿主,回收 Compose 树、释放主线程锁。

关键生命周期方法对照表

方法作用阶段备注
setContent测试方法内设置 Composable 根组件,多次调用会替换树
waitForIdle操作/断言前等待动画、布局、重绘空闲;默认超时 1s
runOnIdle测试方法内在主线程空闲时执行一个操作块
onNodeWithText / onNodeWithTag断言阶段查找节点,返回 SemanticsMatcher
activityRule (AndroidComposeTestRule 特有)任意阶段返回已启动的 Activity 实例

3 实现流程:从声明到断言

以下是一个规范的两步流程模板。先定义规则,再编写测试方法。

// 纯 Compose 单元测试 —— 不依赖 Android 环境
class MyComposeUnitTest {
    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun testButtonClick() {
        composeTestRule.setContent {
            MyButton(text = "提交")
        }

        composeTestRule.onNodeWithText("提交")
            .performClick()

        composeTestRule.onNodeWithText("已点击")
            .assertIsDisplayed()
    }
}

// Android 集成测试 —— 依赖真实 Activity
class MyAndroidComposeTest {
    @get:Rule
    val composeTestRule = AndroidComposeTestRule<MyActivity>(
        activityRule = ActivityScenario.launch(MyActivity::class.java)
    )

    @Test
    fun testActivityContent() {
        composeTestRule.setContent {
            MyScreen()
        }

        composeTestRule.waitForIdle()
        composeTestRule.onNodeWithTag("header").assertExists()
    }
}

夹具隔离的实践要点:如果测试代码中只使用了 setContent、waitForIdle、onNodeWith* 等通用 API,那么同一套测试代码可以直接在两个规则下运行。只需将规则声明从 createComposeRule() 替换为 AndroidComposeTestRule 即可实现从单元测试到集成测试的升级,无需修改业务断言逻辑。

4 调试技巧:规则状态的主动观测

当测试失败表现为“Compose 内容未渲染”或“节点未找到”时,可按以下步骤定位:

  • 打印当前 Compose 树快照:composeTestRule.onRoot().printTo(System.out)
  • 确认宿主是否正常启动:对于 AndroidComposeTestRule,检查 activityRule.scenario.state 是否为 State.RESUMED
  • 超时调整:使用重载 waitForIdle(timeoutMs = 5000) 排除异步延迟干扰
  • 规则冲突检测:确保同一个测试类中只有一个 @get:Rule 标注的 ComposeTestRule,多个规则会导致宿主嵌套启动

5 常见错误与解决方案

错误现象根因解决方案
“No activity” 异常AndroidComposeTestRule 未关联 ActivityScenario使用 AndroidComposeTestRule(ActivityScenario.launch(...)) 构造
“View tree not attached”setContent 调用时 Activity 已销毁检查 @After 中是否调用了 finishScenario();避免在 @Before 中使用 setContent
节点查找超时(1s 不够)Composable 含有动画或延迟加载显式指定 waitForIdle(3000),或使用 useUnrendered = true 参数跳过渲染等待
重复启动 Activity测试类中同时声明了 @Rule ActivityScenarioRule 和 @Rule ComposeTestRule移除多余的 ActivityScenarioRule,由 AndroidComposeTestRule 统一管理

生命周期绑定陷阱:AndroidComposeTestRule 的构造器会立即触发 Activity 启动,而不是等测试方法执行。这意味着如果在 @Before 中依赖 Activity 状态,此时 Activity 已经处于 CREATED 状态。需要手动 moveToState(Lifecycle.State.RESUMED) 才能确保界面完全可见。

6 性能与稳定性优化

  • 减少 waitForIdle 调用次数:每调用一次 waitForIdle 会等待至少一个 frame(≈16ms),连续频繁调用会显著拖慢测试套件。可将多个操作放在一个 runOnIdle 块中批量执行。
  • 使用 useUnrendered 跳过首次渲染:当只测试点击事件而不需要屏幕截图时,在 setContent 中设置 useUnrendered = true 可跳过 Compose 第一帧渲染,节省 50-100ms。
  • 复用 Activity 实例:对于同一 Activity 的多个测试方法,使用 @get:Rule @JvmField val rule = AndroidComposeTestRule(ActivityScenario.launch(...), useActivityAfterTest = true) 避免反复创建和销毁 Activity。

7 生命周期绑定关系图

ComposeTestRule 生命周期绑定流程 JUnit @Before 规则启动宿主 Activity.onCreate / ComposeScene启动 setContent(Composable) 操作 / 断言 JUnit @After 规则销毁宿主 执行顺序 @Before → setContent → 操作 → @After

上图清晰展示了测试规则从 @Before 到 @After 的完整绑定路径:规则先启动宿主,随后测试方法通过 setContent 加载 UI,执行操作和断言,最后在 @After 之后销毁宿主。其中 “规则启动宿主” 和 “规则销毁宿主” 由框架自动完成,开发者只需关注 setContent 与断言逻辑。

8 小结