第3章 Compose UI测试基础:ComposeTestRule

你写过普通的Android UI测试吗?如果写过,大概会对 ActivityTestRule 或 ActivityScenario 不陌生。但在 Compose 的世界里,主角换成了 ComposeTestRule。说白了,它是 Compose 测试的“引擎”——帮你启动 Composable、查找节点、执行操作、验证状态。

我第一次接触 Compose 测试时,最困惑的是:明明写了一个 Button,怎么用常规的 onView(withId(...)) 就找不到了呢?后来才明白,Compose 的 UI 树和传统 View 系统完全不一样。嗯,这里就要靠 ComposeTestRule 来搭桥了。

ComposeTestRule 知识体系 ComposeTestRule 核心知识体系 ComposeTestRule 创建方式 createComposeRule() createAndroidComposeRule() 核心操作 setContent { … } onNodeWithText / onNodeWithTag assertExists / assertIsDisplayed performClick / performTextInput 注意事项 IDLE_TIMEOUT 配置 异步与协程处理 💡 一句话总结 ComposeTestRule 是连接测试代码与 Compose UI 的桥梁,选择合适的创建方式,掌握核心操作,注意异步细节。

3.1 什么是ComposeTestRule?

ComposeTestRule 是 Jetpack Compose 提供的测试规则。它让你的测试代码能像用户一样“看”到 Composable 界面,“点”按钮,“填”输入框,“读”文字。它背后做了很多事:自动帮你 Compose 组件,等待布局稳定,支持协程和动画。

我个人习惯把它理解成一个“微型宿主 Activity”。你在测试里调用 rule.setContent { YourComposable() },Compose 框架就会把它挂载到屏幕上(虽然是虚拟的)。然后你就可以用 onNodeWithText("提交") 这种玩法来定位元素了。

3.2 创建ComposeTestRule的两种方式

你可能会问:我该怎么拿到这个规则呢?两种方式。一张表说清楚:

创建方式 适用场景 示例
createComposeRule() 只需要测试 Composable 本身,不关心 Activity 测试自定义组件、弹窗、普通页面
createAndroidComposeRule<MainActivity>() 需要启动真实 Activity,测试导航、系统交互等 测试 Fragment + Compose 混搭、深层链接

我在项目中遇到过一种情况:用 createAndroidComposeRule 测一个简单的 Composable,结果每次运行都要等 Activity 完全创建——慢!后来换成 createComposeRule,测试执行速度快了一倍。所以我的建议是:除非你非要 Activity 的生命周期不可,否则优先用轻量版。

3.3 核心API速览

记住这几个操作,就能搞定 80% 的回归测试:

  • setContent —— 把要测的 Composable 摆上舞台。你可以传任意 Composable。
  • onNodeWithText / onNodeWithTag —— 定位节点。文本定位最常用;Tag 需要你在 Composable 里用 Modifier.testTag() 预先声明。
  • assertExists() / assertIsDisplayed() —— 断言节点存在或可见。这是回归测试的“定海神针”。
  • performClick() / performTextInput() —— 模拟用户点击、输入。注意:点击后可能需要等待异步完成。
  • waitForIdle() —— 强制等待 Compose 闲下来。虽然不常用(规则会自带等待),但遇到动画或协程抖动时可以手动调用。

3.4 实战:第一个测试用例

光说不练假把式。我们直接写一个测试:假设你有一个登录按钮和一个文本。

@RunWith(AndroidJUnit4::class)
class LoginButtonTest {

    @get:Rule
    val rule = createComposeRule()

    @Test
    fun loginButton_displaysCorrectText() {
        // 1. 设置界面
        rule.setContent {
            LoginScreen() // 你的 Composable
        }

        // 2. 找到按钮
        val button = rule.onNodeWithText("登录")

        // 3. 断言它存在
        button.assertExists()

        // 4. 点击
        button.performClick()

        // 5. 验证结果(比如登录后提示文字变了)
        rule.onNodeWithText("欢迎回来").assertIsDisplayed()
    }
}

你看,整个过程像在写“用户操作说明书”。这也是回归测试的美妙之处——你描述一遍流程,机器替你反复执行。

3.5 常见陷阱与最佳实践

这里有三个我踩过的坑,分享给你:

⚠️ 忘记 testTag 导致定位困难
我曾经在测试一个复杂的 LazyColumn 列表时,里边的条目没有加 Modifier.testTag(),结果用 onNodeWithText 找了半天,总是匹配到多个节点。后来给每个条目加上唯一的 testTag,一秒定位。

💡 异步操作要加 waitUntil
如果你的 Composable 里有协程或 Retrofit 请求,直接 assertExists 可能因为动画还没结束而失败。建议用 rule.waitUntil { condition } 或 waitForIdle()。我个人习惯在点击后立刻调一次 rule.waitForIdle(),保险。

⚠️ IDLE_TIMEOUT 不够长
默认超时是 1 秒。如果你的测试里包含 3D 动画或大数据加载,会因超时而报错。你可以通过 @get:Rule val rule = createComposeRule().apply { idleTimeout = 2000L } 调整。

最后,记得测试代码要 优雅且稳定。回归测试最怕“间歇性失败”——今天绿、明天红。我建议你多用 assertIsDisplayed 而非 assertExists,因为前者会检查节点是否在屏幕上可见(避免 off-screen 误判)。

本章核心回顾

  • ComposeTestRule 是 Compose 测试的基石,帮你启动、查找、交互、断言。
  • 轻量场景用 createComposeRule(),需要 Activity 时用 createAndroidComposeRule。
  • 核心操作:setContent → onNodeWithText/Tag → performClick → assertExists/IsDisplayed。
  • 注意异步等待、testTag 命名规范、超时配置。