2. Compose UI 测试基础:ComposeTestRule 的用法、Robolectric 与 Instrumentation 测试的区别
UI 测试是快照回归测试的根基。你连 UI 测试跑不起来,快照就无从谈起。这一节我们重点盯住两个核心问题:ComposeTestRule 怎么用,以及跑在 Robolectric 上还是跑在真机/模拟器上。
一句话总结:ComposeTestRule 是 Compose 测试的“遥控器”;Robolectric 和 Instrumentation 是两套不同的执行环境。选错了环境,你的快照可能变得不稳定或者慢到离谱。
2.1 ComposeTestRule —— 测试的“遥控器”
写 Compose UI 测试,你几乎离不开 createComposeRule() 或 createAndroidComposeRule()。我个人习惯直接用 createComposeRule(),因为它不绑定 Activity,轻量且更能聚焦在 Composable 组件本身上。
class MyComponentTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun testGreetingDisplayed() {
composeTestRule.setContent {
Greeting(name = "World")
}
composeTestRule.onNodeWithText("Hello, World!")
.assertIsDisplayed()
}
}
这段代码你看着眼熟吧?setContent 会把 Composable 塞进测试环境,后面用 onNodeWithText 找元素,最后断言可见。嗯,核心三板斧。
- setContent —— 加载你要测的 Composable。
- onNode / onNodeWithText / onNodeWithTag —— 通过文本、tag 或 matcher 定位节点。
- performClick / performGesture / assertIsDisplayed / assertTextEquals —— 做操作和断言。
我的一点经验:别滥用 onAllNodes。它在列表或重复组件里确实方便,但匹配不到精准节点时容易在复杂布局里踩坑。能用 onNodeWithTag 就用 tag,毕竟是开发者自己埋的锚点。
2.2 Instrumentation 测试 —— 真实,但慢
Instrumentation 测试跑在物理设备或模拟器上。它需要安装 APK、启动 Activity、渲染真实界面。好处是什么?你看到的和用户看到的几乎一样。快照测试里,某些主题、字体、硬件加速效果只能在 Instrumentation 环境里原样呈现。
@RunWith(AndroidJUnit4::class)
class MyInstrumentationTest {
@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()
@Test
fun testFullScreenSnapshot() {
composeTestRule.onNodeWithTag("mainContent")
.assertExists()
// 这里可以用 PaperCut 或 Roborazzi 截取全屏快照
}
}
注意 createAndroidComposeRule 需要指定 Activity 类,它会自动启动 Activity。对于快照测试,这其实是双刃剑:你得到了真实渲染,但每次启动 Activity 会消耗 5-15 秒。
我曾经踩过的坑:团队在 CI 上跑 Instrumentation 快照,一次测试 300 个组件,等了一个多小时。后来发现三分之一的测试根本不需要真实设备。所以… 能跑 Robolectric 就不要上 Instrumentation,除非你有特殊需求比如 WebView 或系统弹出框。
2.3 Robolectric 测试 —— 快,但有沙盒
Robolectric 是在 JVM 上运行的测试框架。它通过模拟 Android 框架的 Shadow 对象来绕过真机。说白了,没设备也能跑。
对于 Compose 快照来说,Robolectric 是主流方案。它安装快、启动快、截屏也快。而且它支持 Compose 所依赖的很多 API,比如 Context、Resources、View 系统。
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class MyRobolectricTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun testButtonSnapshot() {
composeTestRule.setContent {
MyButton(text = "Submit")
}
// Robolectric 下截图:使用 Roborazzi 或 PaperCut
}
}
注意 @Config(sdk = [34]) —— 我建议显式指定 SDK 版本,否则 Robolectric 会用默认值,可能和你 CI 上的 Android API 不一致,导致快照差异。
2.4 两者对比(一张表搞定)
| 维度 | Robolectric | Instrumentation |
|---|---|---|
| 运行位置 | JVM(本地或 CI) | 真机 / 模拟器 |
| 启动速度 | 秒级 | 10秒~1分钟 |
| 渲染准确性 | 接近,但有 Shadow 偏离 | 完全真实 |
| 系统 UI | 有限支持(通知栏等不真实) | 完全支持 |
| 常用快照库 | Roborazzi, PaperCut | Roborazzi (Instrumentation mode) |
| 推荐场景 | 组件级快照、回归预防 | 端到端、系统交互快照 |
我的建议:把组件级快照(比如按钮、卡片、列表项)放在 Robolectric 上跑;把涉及 Activity、Fragment 生命周期或系统弹框的测试留给 Instrumentation。你想想看,80% 的快照测试其实都可以用 Robolectric 搞定。
2.5 避坑指南:选择环境时的常见错误
- 不要在 Robolectric 下测试平台依赖的 API,比如
Animation或WindowInsets在某些版本里表现不同。 - 不要把所有快照都扔进 Instrumentation,CI 会骂人的。
- 记得在 Robolectric 测试里加上
@GraphicsMode(GraphicsMode.Mode.NATIVE),否则截出来的图可能只有像素化的色块 —— 这是我早期遇到的让人无语的问题。
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@RunWith(RobolectricTestRunner::class)
class MyRobolectricSnapshotTest {
// ...
}
小技巧:如果你在 Robolectric 下截出的字体和 Instrumentation 下不一样(常见于华为、小米等定制 ROM),可以在测试类上写 @Config(fontScale = 1.0f) 来固定字体缩放。保持一致性,快照才不会假失败。
好了,这一节我们把 ComposeTestRule 的用法和两种运行环境的区别说清楚了。记住一点:快照测试的核心是稳定、快速、可复现。环境选对了,后面的工作才省心。