Compose 测试基础
说到 Compose 测试,很多人第一反应是:“UI 都变声明式了,测试还能用原来那套吗?”其实可以,而且比想象中更顺畅。
我刚开始接触 Compose 测试的时候也踩过坑。习惯性地找 id,发现根本没 id 可以设。后来才明白 —— Compose 用的是另一套东西:Semantics 树。
这一章,我们就把它拆开揉碎了讲。
ComposeTestRule:一切的起点
你写 Espresso 测试的时候,得先弄个 ActivityTestRule。Compose 也一样,只是换了个东西叫 ComposeTestRule。
我个人的习惯是,在测试类里这么声明:
@get:Rule
val composeTestRule = createComposeRule()
或者你想直接启动某个 Activity:
@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()
这两个区别在哪?
createComposeRule 只提供 Compose 环境,你自己把组件塞进去;
createAndroidComposeRule 会启动一个 Activity,适合集成测试。
小提示:我一般在单元测试用前者,在 UI 冒烟测试用后者。看你具体场景选。
查找节点:findNode 与 Semantics 树
Compose 没有 R.id.xxx。那怎么定位 UI 组件?
答案是:通过 Semantics 树。每个可交互的 Composable 都会在语义树上注册一个节点,节点带有一堆属性:text、contentDescription、role、state 等等。
你想想看,这不就跟 Web 的 Accessibility Tree 有点像?没错,Compose 的测试框架就是基于无障碍语义树做的。
查找节点的核心函数是 onNode,配合各种 matcher:
onNodeWithText("提交")—— 找文本为“提交”的节点onNodeWithContentDescription("头像")—— 找 content descriptiononNode(hasTestTag("myButton"))—— 通过 testTag 定位onNode(hasClickAction())—— 找到所有可点击的节点
我在项目里遇到过一个问题:俩按钮文本一模一样,结果 onNodeWithText 返回了多个匹配,测试直接挂了。后来我改用 onNode(hasText("提交") and hasClickAction()) 来缩小范围才解决。
// 更精准的查找
composeTestRule.onNode(
hasText("提交") and hasClickAction()
)
避坑指南:我曾经忘了给列表项加 testTag,结果滚动后节点被回收,测试一直找不对位置。后来学乖了 —— 在可复用的 Composable 上统一加 Modifier.semantics { testTag = "item_$id" }。
断言操作:assert
找到节点之后,就得判断它是不是符合预期。Compsoe 提供了一系列 assert 方法:
| 断言方法 | 作用 |
|---|---|
.assertExists() | 节点是否存在 |
.assertDoesNotExist() | 节点是否不存在 |
.assertIsDisplayed() | 是否可见 |
.assertTextEquals("提交") | 文本精准匹配 |
.assertContentDescriptionEquals("关闭") | 描述匹配 |
.assertIsEnabled() / .assertIsNotEnabled() | 是否启用 |
举个实际例子:
composeTestRule
.onNodeWithText("登录")
.assertIsDisplayed()
.assertIsEnabled()
说白了,三步走:找节点 → 断言 → 执行。顺序别搞反了。
执行操作:perform
光断言还不够,你得模拟用户操作吧?
perform 就是干这个的。
常用的操作:
performClick()—— 点击performTextInput("hello")—— 输入文字performScrollTo()—— 滚动到可见performGesture { swipeLeft() }—— 手势操作
看一段完整测试:
composeTestRule
.onNodeWithContentDescription("用户名")
.performTextInput("testuser")
composeTestRule
.onNodeWithText("登录")
.performClick()
composeTestRule
.onNodeWithText("欢迎回来")
.assertExists()
这里有个细节:performTextInput 不会清除已有文本。如果你要替换文本,得先调 performTextClearance()。我刚开始不知道,测试一直失败,排查了半天才发现是输入框里残留了占位符。
重点总结:
- Compose 测试借助 Semantics 树 定位 UI,而不是 View ID。
- 用
onNode+ matcher 找节点,用assert*做检查,用perform*模拟操作。 - 优先给重要组件加
testTag,避免匹配冲突。 - 记得用
and/or组合多个 matcher,提高查找精度。
嗯,这一章的内容先到这。你在写混合测试的时候,如果遇到 View 和 Composable 混在一起的情况,别慌 —— 后面我们会专门讲用 onView 和 onNode 双引擎协同作战。先把 Compose 基础打牢了,后面自然顺手。