第2章 Compose UI Test核心API概览

2.1 原理与背景

Compose UI Test 以 ComposeTestRule 作为测试入口,通过 onNode、onRoot 定位可组合项(Composable),再借助 performGesture 模拟手势操作,最后用 waitFor 系列方法等待异步状态或UI更新。理解这些API的职责与配合方式,是编写可靠的中后台刷新测试的基础。

Compose UI Test 核心API关系图 ComposeTestRule onNode / onNodes onRoot / onAllNodes waitFor… / Idling performGesture… assert / assertExists 箭头方向:从测试规则出发,经过定位、等待、手势、断言形成完整测试链路

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)?