一、Compose UI 测试概述:你在测什么?
大家好,我是老张。做 Android 测试有些年头了,从早期的 Instrumentation 测试,到 Espresso,再到 Compose UI Test,一路踩坑过来。今天咱们聊聊 Compose UI 测试最基础的问题——它到底是什么?跟 View 系统那套测试有啥区别?又到底为什么非做不可?
我个人理解,Compose UI 测试,说白了就是验证你用 Compose 搭出来的界面好不好用、对不对。你写了个 Button,点了能不能跳转?写了个列表,滑起来会不会卡?这些事情,光靠肉眼盯着模拟器看,早晚要翻车。
一句话定义:Compose UI Test 是基于 Compose 运行时提供的语法树(Semantic Tree),通过模仿用户操作来验证界面行为的一种自动化测试方式。
跟 View 系统测试的区别
我当年从 View 系统切到 Compose 时,第一个感觉就是——测试写法完全变了。以前用 Espresso 测 View,你得先 onView(withId(R.id.btn_login)),然后 .perform(click())。现在呢?直接用 composeTestRule.onNodeWithText("登录")。你想想看,连 id 都不需要传了,直接拿文字找节点。
但区别远不止语法。我列个表格,你看一眼就明白:
| 对比维度 | View 系统测试 (Espresso) | Compose UI Test |
|---|---|---|
| 渲染机制 | 基于 View Hierarchy,层层嵌套 | 基于 Semantic Tree,扁平语义 |
| 查找元素 | 靠 id、class、text 匹配 | 靠语义属性(文本、内容描述、标签) |
| 异步处理 | 需要手动 IdlingResource | 内置 Compose 协程调度,自动等待 |
| 代码结构 | 测试代码和 UI 代码分离 | 更建议写在一起,测试即文档 |
我个人的小建议:如果你团队还在用 View 系统,别急着全量迁移。可以先拿一个页面试试 Compose + UI Test 的写法,感受下语义树带来的清爽感。
为什么需要 UI 测试?
这个问题,我遇到过好多次。有些兄弟觉得:我有单元测试就够了,UI 测试又慢又脆。嗯,这话只说对了一半。
单元测试确实能覆盖业务逻辑,但它检查不了两件事:
- 布局对不对——比如你传了个空列表,页面展示的是“暂无数据”还是白屏一片?
- 交互响应——用户点错按钮、快速连点、滑动冲突,这些真实场景单元测试根本模拟不了。
我记得有一次做电商项目,我们单元测试全绿,结果上线第二天,用户反馈「购物车页面点加号闪退」。排查后发现是某个 composeable 的 animation 回调里访问了一个空对象。你说单元测试能测到?根本不可能。后来我们补了一个简单的 UI 测试,模拟点击加号 10 次,立刻复现 bug。
避坑指南:不要指望 UI 测试发现所有问题。它最适合的场景是——核心流转(登录、下单、支付)和边界状态(空数据、网络异常)。我之前就是踩了「全量覆盖」的坑,后来老老实实按优先级写。
所以,你想想看,UI 测试不是用来替代单元测试的,它是最后一道防线。尤其 Compose 这种声明式 UI,状态一变整个视图重建,光靠人眼看代码很难发现拼接错误。有了 UI 测试,至少能保证「用户看到的」和「你预期的」是一致的。
Compose UI 测试核心逻辑图
为了让你更直观理解,我画了一张流程图。它把 Compose UI 测试的完整数据流串起来了——从 Compose 渲染,到语义树生成,再到测试框架匹配和执行断言。
看明白了吗?从上往下,Compose 渲染出界面后,框架自动构建语义树。ComposeTestRule 就是你的遥控器,通过它去树上找节点(比如找文字「登录」),然后要么断言它存在,要么模拟点击。整个过程,你都不需要关心控件 id 或者布局层级——这就是 Compose UI 测试最爽的地方。
简单体验一下代码
光说不练假把式。我随手写一个最简单的测试,你先感受下:
class LoginScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun loginButton_shouldBeDisplayed() {
composeTestRule.setContent {
LoginScreen()
}
// 查找「登录」按钮
composeTestRule.onNodeWithText("登录")
.assertExists() // 断言按钮存在
.assertIsDisplayed() // 断言按钮可见
}
}
嗯,对,核心就是三个动作:setContent → onNode → assert。后期你还能加 performClick、performGesture 等交互行为。别着急,后面的章节我会手把手带你拆解每个 API 背后的原理和坑。
个人经验:刚开始不要追求复杂场景。先把你 App 里三个最核心的页面写个“存在性测试”——只检查关键文案或按钮是不是显示出来了。这一步做到,就能拦截 80% 的线上布局问题。