4. Compose测试框架入门:@ComposeTestRule、createComposeRule、setContent基本用法

终于到了动手写测试的真章了。很多刚入坑Compose的朋友会问我:「测试框架到底怎么起手?」说白了,就三个核心东西:ComposeTestRule 接口、createComposeRule 工厂方法,还有 setContent 方法。搞懂它们,你就能把任意Compose组件塞进测试环境里,然后开始验证一切。

4.1 三件套:规则、创建、填充

我们先看最基础的写法。我个人习惯用 createComposeRule() 创建一个规则,然后在测试方法里用 setContent 加载组件。来,上代码:

@RunWith(AndroidJUnit4::class)
class MyComposeTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun testButtonDisplay() {
        composeTestRule.setContent {
            Button(onClick = { }) {
                Text("Click me")
            }
        }
        composeTestRule.onNodeWithText("Click me").assertExists()
    }
}

这段代码你一看就眼熟吧?其实@get:Rule是JUnit4的规则注解,Compose框架把它绑定到测试生命周期。createComposeRule返回的是一个ComposeTestRule实例,它相当于是你测试环境的「遥控器」。

核心要点

  • ComposeTestRule:接口,定义了所有操作和断言方法
  • createComposeRule():工厂函数,创建一个默认的测试规则
  • setContent:在测试上下文中设置Composable内容,就像setContent在Activity里那样

4.2 createComposeRule vs createAndroidComposeRule

你可能会在别人的代码里看到另一种写法:createAndroidComposeRule<MainActivity>()。这两者有什么区别?

特性 createComposeRule createAndroidComposeRule
启动Activity 否,仅创建测试环境 是,启动指定Activity
应用场景 纯Compose组件测试 需要Activity上下文的集成测试
依赖 只需Compose UI测试库 需要AndroidX Activity库
性能 更快(不启动Activity) 稍慢

我的建议:测试纯UI逻辑时优先用createComposeRule。我在项目中遇到过把整个Activity启动起来测一个简单的Text排版,跑一次测试要多花3秒。能省就省。

4.3 setContent到底做了什么?

很多人以为setContent就是直接渲染组件。其实它背后干了几件事:

  1. 创建一个独立的Compose宿主窗口(而不是Activity的DecorView)
  2. 把你的Composable函数挂接到这个窗口的根节点
  3. 启动重组循环,让组件完成第一帧渲染
  4. 等待布局、绘制完成(默认会等待Idle状态)

所以你在setContent之后立刻执行assertExists,能保证组件已经显示在屏幕上。嗯,这里要注意:如果你在里面写了LaunchedEffect或其他异步操作,框架也会等到这些异步执行完(或者超时)才继续。

曾经踩过的坑:我在一个项目里测一个带animateFloatAsState的组件,setContent之后立刻断言动画值——结果失败了。因为动画需要时间驱动。解决办法是用waitForIdle()或者advanceTimeByFrame()。你想想看,动画更新不是同步的,得给框架一点时间。

4.4 测试规则的高级配置

createComposeRule返回的规则默认带了一些好用的能力,比如自动等待空闲、截图失败保存等。但你也可以自定义:

val composeTestRule = createComposeRule(
    useUnlimitedTree = false,
    density = Density(
        fontScale = 1.2f,
        density = 2f
    )
)

useUnlimitedTree控制是否限制组件树深度,默认限制是为了防止内存泄漏。我曾经为了测一个复杂的LazyVerticalGrid把树深调成了true——结果内存撑爆测试机。后来老老实实改成false,用onNodeWithText配合performScrollToIndex定位。

另外一点:density参数可以模拟不同屏幕密度下的显示效果,很适合做多分辨率验证。

4.5 从Activity到独立测试——做个对比

我们之前写的测试如果放在Activity里:

@Test
fun testInActivity() {
    activityScenarioRule.scenario.onActivity { activity ->
        activity.setContent {
            Greeting("Android")
        }
    }
}

对比现在的ComposeTestRule写法:

@Test
fun testWithRule() {
    composeTestRule.setContent {
        Greeting("Android")
    }
    composeTestRule.onNodeWithText("Hello Android").assertExists()
}

哪个更简洁?显然createComposeRule省掉了Activity的中间层,直接操作组件。而且API更直观,onNodeWithText本身就是ComposeTestRule的方法,不用再搞什么onActivity回调。

4.6 知识体系:一条调用链

下面这张图把今天讲的东西串起来,你一看就能明白测试执行的全流程:

Compose测试框架执行流程 createComposeRule() ComposeTestRule 实例 setContent { ... } 渲染 + 等待空闲 onNode...().assert...() 注意:setContent内部会创建一个宿主窗口,组件完全渲染后才返回。 若组件含异步操作(动画、LaunchedEffect),需额外等待处理。

这张流程图画得很清楚:从规则创建到断言执行,总共五步。每一步都依赖上一步的结果。

4.7 常见问题与避坑

  • 问题:setContent里面写了一个没有modifier的组件,为什么找不到?
    答案:检查下你是否传了testTag或者用文本匹配。如果没有可辨识的内容,onNodeWithText肯定找不到。我曾经因为用一个空的Box去测试点击,结果怎么都找不到节点——后来加了Modifier.testTag("empty-box")才解决。
  • 问题:测试跑起来超慢,一秒跑一个。
    避坑:检查是不是每次测试都重新创建Activity。建议纯UI测试用createComposeRule,别启动Activity。
  • 问题:setContent里用remember保存的状态在多个测试间共享吗?
    答:不会。每个@Test方法都是独立的,规则会重置所有的组合状态。别担心污染。

一个小技巧:在setContent之前,如果你想修改默认密度或主题,可以用composeTestRule.setContent(theme = MyTheme) { ... }。我之前测试深色模式下的对比度,就直接在里面套了一层MyTheme(darkTheme = true)。比跑到系统设置里改省事多了。

4.8 小结一下