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就是直接渲染组件。其实它背后干了几件事:
- 创建一个独立的Compose宿主窗口(而不是Activity的DecorView)
- 把你的Composable函数挂接到这个窗口的根节点
- 启动重组循环,让组件完成第一帧渲染
- 等待布局、绘制完成(默认会等待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 知识体系:一条调用链
下面这张图把今天讲的东西串起来,你一看就能明白测试执行的全流程:
这张流程图画得很清楚:从规则创建到断言执行,总共五步。每一步都依赖上一步的结果。
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)。比跑到系统设置里改省事多了。