Compose 测试基础

说到 Compose 测试,很多人第一反应是:“UI 都变声明式了,测试还能用原来那套吗?”
其实可以,而且比想象中更顺畅。

我刚开始接触 Compose 测试的时候也踩过坑。习惯性地找 id,发现根本没 id 可以设。后来才明白 —— Compose 用的是另一套东西:Semantics 树。

这一章,我们就把它拆开揉碎了讲。

Compose 测试核心知识体系 ComposeTestRule findNode 🔍 assert ✅ perform 🖱️ Semantics 树(语义树) onNode(matcher) (节点匹配器) hasContentDescription / hasText (语义属性) assert 与 perform 都基于 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 description
  • onNode(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 基础打牢了,后面自然顺手。