compose-ui-testing-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCompose: UI testing patterns
Compose:UI测试模式
Core principle
核心原则
Test the smallest UI contract that proves the behavior. Prefer plain state-driven UI tests with callbacks. Add integration only when lifecycle, navigation, DI, or platform behavior is the thing under test.
测试能够验证行为的最小UI契约。优先选择带回调的纯状态驱动UI测试。仅当需要测试生命周期、导航、DI(依赖注入)或平台行为时,才进行集成测试。
Test target choice
测试目标选择
| What you need to prove | Test shape |
|---|---|
| Text, button, loading/error branch, conditional content | Plain UI Compose test |
| Callback wiring from click/input | Plain UI Compose test |
| Focus navigation or keyboard behavior | Compose test with key input |
| Visual layout, clipping, elevation, typography, image composition | Screenshot test |
| State holder updates UI correctly | State holder/unit test plus one wiring smoke test |
| Navigation, lifecycle, DI integration | Integration test |
| 需要验证的内容 | 测试类型 |
|---|---|
| 文本、按钮、加载/错误分支、条件内容 | 纯UI Compose测试 |
| 点击/输入的回调连接 | 纯UI Compose测试 |
| 焦点导航或键盘行为 | 带按键输入的Compose测试 |
| 视觉布局、裁剪、阴影高度、排版、图片合成 | 截图测试 |
| 状态持有者能否正确更新UI | 状态持有者/单元测试 + 一个连接冒烟测试 |
| 导航、生命周期、DI集成 | 集成测试 |
Prefer plain UI tests
优先选择纯UI测试
If the screen has a state holder/UI split, test the plain UI composable:
kotlin
composeTestRule.setContent {
ProfileScreen(
state = ProfileUiState(name = "Ada", canSave = true),
onNameChange = {},
onSaveClick = { saved = true },
onBackClick = {},
)
}
composeTestRule.onNodeWithText("Ada").assertIsDisplayed()
composeTestRule.onNodeWithText("Save").performClick()
assertThat(saved).isTrue()This avoids constructing ViewModels, components, repositories, navigation, and dependency graphs for layout behavior.
如果屏幕采用状态持有者与UI分离的架构,测试纯UI组件:
kotlin
composeTestRule.setContent {
ProfileScreen(
state = ProfileUiState(name = "Ada", canSave = true),
onNameChange = {},
onSaveClick = { saved = true },
onBackClick = {},
)
}
composeTestRule.onNodeWithText("Ada").assertIsDisplayed()
composeTestRule.onNodeWithText("Save").performClick()
assertThat(saved).isTrue()这样可以避免为了测试布局行为而构建ViewModel、组件、仓库、导航和依赖图。
Semantics first
优先使用语义断言
Assert semantics when behavior is semantic:
- Text exists: .
onNodeWithText - Button is enabled/disabled: ,
assertIsEnabled.assertIsNotEnabled - Content is selected/focused/toggled: use semantics assertions.
- Content is absent: .
assertDoesNotExist
Use test tags for nodes that have no stable user-visible text or where multiple nodes share text. Do not use tags as the first choice for all assertions; user-visible semantics are usually stronger.
当行为属于语义范畴时,使用语义断言:
- 文本存在:.
onNodeWithText - 按钮启用/禁用:,
assertIsEnabled.assertIsNotEnabled - 内容被选中/聚焦/切换:使用语义断言.
- 内容不存在:.
assertDoesNotExist
对于没有稳定用户可见文本或多个节点共享相同文本的节点,使用测试标签。不要将标签作为所有断言的首选;用户可见的语义通常更可靠。
Callback testing
回调测试
Use simple counters or captured values:
kotlin
var selectedId: String? = null
composeTestRule.setContent {
ItemList(
items = listOf(ItemUi("movie-1", "Movie")),
onItemClick = { selectedId = it },
)
}
composeTestRule.onNodeWithText("Movie").performClick()
assertThat(selectedId).isEqualTo("movie-1")For plain captured callback values, a direct assertion after the action is usually enough. Use when the assertion needs Compose to finish applying snapshot state, recomposition, or queued UI work before reading the result.
runOnIdle使用简单计数器或捕获值:
kotlin
var selectedId: String? = null
composeTestRule.setContent {
ItemList(
items = listOf(ItemUi("movie-1", "Movie")),
onItemClick = { selectedId = it },
)
}
composeTestRule.onNodeWithText("Movie").performClick()
assertThat(selectedId).isEqualTo("movie-1")对于普通的捕获回调值,在操作后直接断言通常就足够了。当断言需要Compose完成快照状态应用、重组或排队的UI工作后才能读取结果时,使用。
runOnIdleKeyboard and focus
键盘与焦点
For keyboard, TV, and desktop UI, drive navigation with the same input model users use (keys/D-pad), not clicks alone. Assert focused semantics, not colors or scale; reserve screenshots for visual focus treatment.
Details—focus graph, , restoration, key handlers, and test patterns: .
FocusRequestercompose-focus-navigation针对键盘、电视和桌面UI,使用与用户相同的输入模型(按键/方向键)来驱动导航,而不仅仅是点击。断言聚焦语义,而非颜色或缩放;截图仅用于验证视觉焦点样式。
详细信息——焦点图、、焦点恢复、按键处理器和测试模式:。
FocusRequestercompose-focus-navigationScreenshot tests
截图测试
Use screenshots for visual contracts that semantics cannot prove:
- Layout spacing/alignment.
- Themed colors, typography, elevation, shadows.
- Image composition, gradients, overlays.
- Focus highlight appearance.
- Loading skeletons or dense visual states.
Keep screenshot state deterministic:
- Use fixed state data.
- Freeze clocks or animation progress when possible.
- Replace network/image loading with fake or preview handlers.
- Avoid asserting dynamic text such as current time unless controlled.
对于语义无法验证的视觉契约,使用截图测试:
- 布局间距/对齐方式.
- 主题颜色、排版、阴影高度、阴影.
- 图片合成、渐变、覆盖层.
- 焦点高亮样式.
- 加载骨架或密集视觉状态.
确保截图测试的状态是确定的:
- 使用固定状态数据.
- 尽可能冻结时钟或动画进度.
- 使用模拟或预览处理器替代网络/图片加载.
- 避免断言动态文本(如当前时间),除非能对其进行控制.
Fake images and platform services
模拟图片与平台服务
When image content is irrelevant, fake the loader and assert the requested model if that is the behavior. The exact hook depends on your image library; a project helper might look like this:
kotlin
val requestedModels = mutableListOf<Any?>()
// Example helper, not a Compose API.
setContentWithFakeImageLoader { request ->
requestedModels += request.data
errorPainter()
}When image appearance matters, provide a deterministic local painter/bitmap instead of network data.
当图片内容无关紧要时,模拟加载器并断言请求的模型(如果这是需要验证的行为)。具体的钩子取决于你的图片库;项目中的辅助工具可能如下所示:
kotlin
val requestedModels = mutableListOf<Any?>()
// Example helper, not a Compose API.
setContentWithFakeImageLoader { request ->
requestedModels += request.data
errorPainter()
}当图片外观很重要时,提供确定的本地painter/位图,而非网络数据。
Common mistakes
常见错误
| Mistake | Fix |
|---|---|
| Constructing full app graph to test an error row | Test plain UI with |
| Testing click behavior through a ViewModel mock | Pass a callback and assert it was invoked |
| Screenshot test for simple text presence | Use semantics assertion |
| Semantics test for padding/color/focus ring | Use screenshot test |
| Test tags everywhere | Prefer text/content description/role when stable |
| UI test depends on real image loading/network/time | Fake or freeze the source |
TV/keyboard UI tested with | Use key input and focus assertions; see compose-focus-navigation |
| 错误做法 | 修复方案 |
|---|---|
| 构建完整应用图来测试错误行 | 使用 |
| 通过ViewModel mock测试点击行为 | 传入回调并断言其被调用 |
| 用截图测试验证简单文本存在 | 使用语义断言 |
| 用语义测试验证内边距/颜色/焦点环 | 使用截图测试 |
| 到处使用测试标签 | 当文本/内容描述/角色稳定时,优先使用它们 |
| UI测试依赖真实图片加载/网络/时间 | 模拟或冻结这些数据源 |
仅用 | 使用按键输入和焦点断言;参见compose-focus-navigation |
Red flags during review
评审中的预警信号
- "This UI test is flaky because images load slowly."
- A test uses production DI for simple rendering.
- A screenshot has random dates, clocks, remote images, or live data.
- Assertions only check that a node exists after performing an action, not that the callback/state change happened.
- Focus behavior is visually inspected but not asserted.
- "这个UI测试不稳定,因为图片加载缓慢。"
- 测试使用生产环境DI来进行简单渲染。
- 截图包含随机日期、时钟、远程图片或实时数据。
- 断言仅检查执行操作后节点是否存在,而非回调/状态变化是否发生。
- 焦点行为仅通过视觉检查,未进行断言。