第2章 Compose UI Test核心API概览
2.1 原理与背景
Compose UI Test 以 ComposeTestRule 作为测试入口,通过 onNode、onRoot 定位可组合项(Composable),再借助 performGesture 模拟手势操作,最后用 waitFor 系列方法等待异步状态或UI更新。理解这些API的职责与配合方式,是编写可靠的中后台刷新测试的基础。
2.2 完整实现步骤与代码片段
2.2.1 创建ComposeTestRule
使用 createComposeRule() 或 createAndroidComposeRule() 获得测试规则。
@get:Rule
val composeTestRule = createComposeRule()
2.2.2 查找节点(onNode / onRoot)
- onNode(matcher):返回与匹配器匹配的第一个节点。
- onNodes(matcher):返回所有匹配节点的列表。
- onRoot():返回整个组合树的根节点,可用于全局断言。
// 通过文本查找按钮
composeTestRule
.onNode(hasText("刷新"))
.assertExists()
// 通过标签查找
composeTestRule
.onNode(hasContentDescription("刷新图标"))
.performClick()
// 查找所有条目
val allItems = composeTestRule.onNodes(hasTestTag("list_item"))
assertThat(allItems.fetchSemanticsNodes().size, `is`(3))
2.2.3 手势模拟(performGesture)
performGesture 是 SemanticsNodeInteraction 的扩展方法,内部使用 performTouchInput 执行复杂的触摸序列。
// 模拟下拉刷新
composeTestRule.onNode(hasTestTag("pullRefresh"))
.performGesture { swipeDown() }
// 模拟长按后滑动
composeTestRule.onNode(hasText("拖动目标"))
.performGesture {
down()
advanceEventTime(200)
moveBy(Offset(0f, 200f))
up()
}
// 简单点击(便捷方法)
composeTestRule.onNode(hasText("确定")).performClick()
2.2.4 等待异步更新(waitFor)
waitFor系列方法用于等待条件满足,适用于后台任务刷新导致的UI变化。
// 等待条件成立(最多1秒)
composeTestRule.waitFor(500) {
composeTestRule.onNode(hasText("已刷新")).isDisplayed()
}
// 等待节点消失
composeTestRule.waitUntilDoesNotExist(hasText("加载中…"))
// 等待Idle状态(推荐用于后台任务)
composeTestRule.waitForIdle()
2.3 关键配置与API参数
| API | 参数/配置 | 说明 |
|---|---|---|
createComposeRule() | - | 创建纯Compose测试规则,无Activity上下文 |
createAndroidComposeRule<T>() | Activity类 | 创建带Activity的规则,支持上下文相关测试 |
onNode(matcher, useUnmergedTree) | useUnmergedTree: Boolean = false | 设为true可访问合并前的底层语义节点(如文本框内部分段) |
onRoot(useUnmergedTree) | useUnmergedTree: Boolean = false | 获取整个组合树的根 |
performGesture(block) | GestureScope.() -> Unit | 手势闭包,支持click、swipeUp、swipeDown等 |
waitFor(timeoutMs, condition) | timeoutMs: Long = 1000 | 轮询等待条件返回true,超时抛出AssertionError |
waitUntil(condition) | ()->Boolean | 使用默认超时(1秒)等待条件为true |
waitUntilDoesNotExist(matcher) | 匹配器 | 等待匹配的节点从树中移除 |
waitForIdle() | - | 等待主线程空闲(所有后台任务完成) |
2.4 常见问题与调试技巧
⚠️ 常见陷阱:waitFor vs waitForIdle
waitFor 只检查条件是否满足,不会等待LaunchedEffect或snapshotFlow触发的协程完成。对于后台任务刷新,应优先使用waitForIdle()或等待特定状态节点出现。
💡 调试技巧:开启语义树日志
在测试类中添加:
在测试类中添加:
composeTestRule.mainClock.autoAdvance = false
composeTestRule.mainClock.advanceTimeBy(16) // 手动推进帧
使用printToLog()打印完整语义树:
composeTestRule.onRoot().printToLog("DEBUG_TREE")
✅ 稳定定位建议
- 优先使用
hasTestTag()而非文本匹配(避免国际化影响)。 - 对动态列表使用
onNodes()配合index参数。 - 避免在主线程中执行长时间等待,使用
runBlockingTest管理协程。
2.5 可验证的测试结果与日志
2.5.1 测试用例示例:后台数据刷新后验证列表更新
@Test
fun testRefreshedListAppears() {
// 模拟按钮触发后台请求
composeTestRule.onNode(hasText("获取数据")).performClick()
// 等待加载中指示器消失
composeTestRule.waitUntilDoesNotExist(hasText("加载中"))
// 等待后台任务完成
composeTestRule.waitForIdle()
// 断言列表出现
composeTestRule.onNode(hasTestTag("data_list")).assertExists()
// 验证列表项个数
val items = composeTestRule.onNodes(hasTestTag("list_item"))
assertThat(items.fetchSemanticsNodes().size, `is`(5))
}
2.5.2 预期日志输出(开启语义树)
DEBUG_TREE: Printing SemanticsTree…
Root:
Node #1 (role: Button) text="获取数据" actions=[Click]
Node #2 (role: Text) text="加载中"
└── Node #3 (role: ProgressIndicator)
Node #4 (role: LazyList) testTag="data_list"
├── Node #5 (role: ListItem) testTag="list_item" text="条目1"
├── Node #6 (role: ListItem) testTag="list_item" text="条目2"
├── Node #7 (role: ListItem) testTag="list_item" text="条目3"
├── Node #8 (role: ListItem) testTag="list_item" text="条目4"
└── Node #9 (role: ListItem) testTag="list_item" text="条目5"
日志中可见加载完成后,load节点消失,data_list及其5个子项出现。如果列表项少于预期,说明后台任务未正确更新状态或测试等待时机过早。
2.5.3 超时失败典型错误
java.lang.AssertionError: Condition still false after 1000 ms
at … waitUntil$default …
at … testRefreshedListAppears … (Test line 28)
遇到此错误时,应检查:
- 是否调用了
waitForIdle()? - 后台任务是否在
Dispatchers.IO上执行?需要runBlockingTest配合测试分发器。 - 匹配器是否准确(如拼写、大小写、TestTag)?