官方文档与代码
Codelab-在 Jetpack Compose 中进行测试
codelab-android-compose/TestingCodelab/app/src/androidTest/java/com/example/compose/rally/AnimatingCircleTests.kt at main · android/codelab-android-compose
代码库中 在android stuido 2026.1.2
import androidx.compose.ui.test.junit4.createComposeRule
改为
imp
注意,有的设备要设置才可以运行 测试用例
比如小米的note13 我运行测试用例就一直卡死
用了android studio 自带的模拟器 pixel 10 才成功把 单元测试运行到手机上
运行效果

直接测试Text

测试RallyTopAppBar
代码
package com.example.compose.rally
import androidx.compose.material.Text
import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasParent
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import com.example.compose.rally.ui.components.RallyTopAppBar
import org.junit.Rule
import org.junit.Test
class TopAppBarTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun myTest() {
composeTestRule.setContent {
Text("You can set any Compose content!")
}
Thread.sleep(5000)
}
@Test
fun rallyTopAppBarTest_currentTabSelected() {
composeTestRule.setContent {
RallyTopAppBar(
allScreens = RallyScreen.entries,
onTabSelected = { },
currentScreen = RallyScreen.Accounts
)
}
composeTestRule.onRoot().printToLog("currentLabelExists") // 在logat输出语义树
Thread.sleep(5000)
composeTestRule
.onNodeWithContentDescription(RallyScreen.Accounts.name)
.assertIsSelected()
}
@Test
fun rallyTopAppBarTest_currentLabelExists() {
composeTestRule.setContent {
RallyTopAppBar(
allScreens = RallyScreen.entries,
onTabSelected = { },
currentScreen = RallyScreen.Accounts
)
}
composeTestRule
.onNode(
hasText(RallyScreen.Accounts.name.uppercase()) and
hasParent(
hasContentDescription(RallyScreen.Accounts.name)
),
useUnmergedTree = true
)
.assertExists()
}
}
logcat的日志情况

image.png
D printToLog:
Printing with useUnmergedTree = 'false'
Node #1 at (l=0.0, t=142.0, r=1080.0, b=289.0)px
|-Node #2 at (l=0.0, t=142.0, r=1080.0, b=289.0)px
IsContainer = 'true'
Shape = 'RectangleShape'
|-Node #3 at (l=0.0, t=142.0, r=1080.0, b=289.0)px
[SelectableGroup]
|-Node #4 at (l=42.0, t=184.0, r=105.0, b=247.0)px
| ContentDescription = '[Overview]'
| Focused = 'false'
| Role = 'Tab'
| Selected = 'false'
| Shape = 'RectangleShape'
| Actions = [OnClick, RequestFocus]
| MergeDescendants = 'true'
| ClearAndSetSemantics = 'true'
|-Node #6 at (l=189.0, t=184.0, r=469.0, b=247.0)px
| ContentDescription = '[Accounts]'
| Focused = 'false'
| Role = 'Tab'
| Selected = 'true'
| Shape = 'RectangleShape'
| Actions = [OnClick, RequestFocus]
| MergeDescendants = 'true'
| ClearAndSetSemantics = 'true'
|-Node #10 at (l=553.0, t=184.0, r=616.0, b=247.0)px
ContentDescription = '[Bills]'
Focused = 'false'
Role = 'Tab'
Selected = 'false'
Shape = 'RectangleShape'
Actions = [OnClick, RequestFocus]
MergeDescendants = 'true'
ClearAndSetSemantics = 'true'
这是 Compose 的语义树(Semantics Tree),是 UI 测试的核心。我来逐层解析:
整体结构
Node #1 (根) → 整个屏幕
└── Node #2 → 内容区域
└── Node #3 → SelectableGroup (Tab 容器)
├── Node #4 → Overview Tab
├── Node #6 → Accounts Tab ← 当前选中
└── Node #10 → Bills Tab
关键属性解读
以 Accounts Tab (Node #6) 为例:
| 属性 | 值 | 含义 |
|---|---|---|
ContentDescription = '[Accounts]' |
Accounts |
节点的无障碍描述,测试用 onNodeWithContentDescription("Accounts") 找的就是它 |
Role = 'Tab' |
Tab | 组件角色,说明这是个 Tab 组件 |
Selected = 'true' |
✅ true | 当前选中的 Tab |
Focused = 'false' |
false | 没有输入焦点 |
MergeDescendants = 'true' |
true | 子节点的语义合并到父节点 |
ClearAndSetSemantics = 'true' |
true | 清除了默认语义,手动设置了上面的属性 |
Actions = [OnClick, RequestFocus] |
支持的操作:点击、请求焦点 |
三个 Tab 对比
| Tab | ContentDescription | Selected | 状态 |
|---|---|---|---|
| Overview | [Overview] |
false |
未选中 |
| Accounts | [Accounts] |
true |
✅ 当前选中 |
| Bills | [Bills] |
false |
未选中 |
为什么测试能通过
composeTestRule
.onNodeWithContentDescription(RallyScreen.Accounts.name) // → 找到 Node #6
.assertIsSelected() // → 检查 Selected = 'true'
完全匹配! Accounts 节点的 Selected 确实是 true。
MergeDescendants = 'true' 的含义
Node #6 (Accounts Tab) ← 合并了所有子节点语义
└── 子节点(图标 + 文字)
└── 语义被合并到父节点,不单独显示
所以你在语义树里看不到 Accounts 内部的 Text 节点,因为它们被合并到 Node #6 了。
这也是第二个测试需要 useUnmergedTree = true 的原因:
composeTestRule.onNode(
hasText("ACCOUNTS") and hasParent(hasContentDescription("Accounts")),
useUnmergedTree = true // ← 不合并,才能看到内部的 Text 节点
)
坐标信息
Node #6 at (l=189.0, t=184.0, r=469.0, b=247.0)px
↑left ↑top ↑right ↑bottom
计算:
- 宽度 = 469 - 189 = 280px
- 高度 = 247 - 184 = 63px
这是 Accounts Tab 在屏幕上的实际位置和大小。
一句话总结
语义树 = Compose UI 的"无障碍视图",测试框架通过它查找和验证节点,而不是直接操作像素。
printToLog把这个树打印出来,让你看到每个节点的属性和层级关系。