关于 compose-ui-testing-patterns
一套 Compose UI 测试技能,指导开发者围绕最小行为契约编写测试。它强调受控状态和语义断言,仅在必要时采用集成或截图测试。
主要功能
- 01选择最小测试切入点
- 02基于状态的 Compose UI 测试
- 03语义与回调断言
- 04确定性测试同步
- 05截图与焦点测试指引
适用场景
- 01测试组件的加载或错误分支
- 02检查点击后的回调行为
- 03验证键盘焦点导航
- 04记录视觉布局契约
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ chrisbanes/ skills --skill compose-ui-testing-patterns安装后在 Claude Code 中输入/compose-ui-testing-patterns确认已加载。
附带文件references/interaction-state.md
详细介绍
Compose:UI 测试模式
核心原则
测试能够证明行为的最小 UI 契约。优先使用带回调的纯状态驱动 UI 测试。只有当生命周期、导航、DI 或平台行为是测试对象时,才添加集成测试。
流程
- 说明任务要求证明的行为和测试关注点。
- 对照该关注点检查现有测试,并从下表中选择足以满足需求的最小切入点。
- 将修改限制在请求的测试关注点内。不要将仅用于测试的辅助工具移入生产代码,也不要扩大生产 API,除非生产边界本身就是测试对象。
- 驱动受控状态或输入;必要时通过 Compose 同步,然后断言可观察到的语义、视觉或回调结果。
- 如果现有测试已经使用最精简且有效的切入点,并证明了所请求的行为,则不做修改。
选择测试目标
| 需要证明的内容 | 测试形式 |
|---|---|
| 文本、按钮、加载/错误分支、条件内容 | 纯 UI Compose 测试 |
| 点击/输入的回调连线 | 纯 UI Compose 测试 |
| 焦点导航或键盘行为 | 使用按键输入的 Compose 测试 |
| 视觉布局、裁剪、海拔、字体排印、图像组合 | 截图测试 |
| 状态持有者能否正确更新 UI | 状态持有者/单元测试,加一项连线冒烟测试 |
| 悬停、按下、聚焦、拖动交互状态 | 使用 MutableInteractionSource 的纯 UI 测试 |
| 导航、生命周期、DI 集成 | 集成测试 |
优先使用纯 UI 测试
如果屏幕采用状态持有者/UI 分离结构,请测试纯 UI 组合项:
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、组件、存储库、导航和依赖关系图。
优先使用语义断言
当行为涉及语义时,应断言语义:
- 文本存在:
onNodeWithText。 - 按钮已启用/已禁用:
assertIsEnabled、assertIsNotEnabled。 - 内容已选中/已聚焦/已切换:使用语义断言。
- 内容不存在:
assertDoesNotExist。
对于没有稳定用户可见文本的节点,或多个节点具有相同文本的情况,使用测试标签。不要将标签作为所有断言的首选;用户可见语义通常更可靠。
回调测试
使用简单计数器或捕获的值:
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 工作后才能读取结果,则使用 runOnIdle。
保持 UI 测试的确定性
对于布局、分支和回调行为,使用 setContent 渲染受控状态,而不是构造生产应用的依赖关系图。生产环境的 DI、存储库、生命周期观察器和后台副作用会增加与纯 UI 契约无关的异步工作,并可能导致测试不稳定。
不要使用 Thread.sleep 等待 Compose。将 UI 驱动至已知状态,然后使用语义断言和 Compose 同步机制(waitForIdle、runOnIdle,或针对真实异步条件使用有界的 waitUntil)。如果设置了 mainClock.autoAdvance = false,请先推进时钟(mainClock.advanceTimeBy)至预期观察时间,再进行断言;不要在对同一帧进行多项检查时推进时钟。否则,请保持自动推进开启。仅当行为确实依赖导航、生命周期、DI 或平台连线时,才使用完整应用集成测试。
使用 MutableInteractionSource 测试交互状态
当交互状态是关注点时,在编写或审查测试之前,请完整阅读交互状态流程。该流程要求直接注入交互状态,而不是使用脆弱的指针模拟。
键盘和焦点
对于键盘、电视和桌面 UI,使用用户实际采用的同一种输入方式(按键/方向键)驱动导航,而不只是点击。断言焦点语义,而不是颜色或缩放;截图仅用于验证焦点的视觉效果。 如果检查的 composable 未暴露任何可改变状态的输入界面, 请先指出缺少的宿主或父级交互接缝,再提出按键驱动的 测试;不要假定所提供的 UI 中存在某个选择器或控件。
详细信息——焦点图、FocusRequester、恢复、按键处理器和测试模式:compose-focus-navigation。
截图测试
当语义无法证明视觉约定时,使用截图:
- 布局间距/对齐。
- 主题颜色、字体排版、海拔、阴影。
- 图像构图、渐变、叠加层。
- 焦点高亮效果。
- 加载骨架屏或高密度视觉状态。
保持截图状态具有确定性:
- 使用固定状态数据。
- 尽可能冻结时钟或动画进度。
- 使用伪造或预览处理器替代网络/图像加载。
- 避免断言当前时间等动态文本,除非其受控。
截图默认值变更时
- 如果有意更改了命名预设或默认值,请在测试中保留该更改并更新 基准图。不要改用原始值来保留旧图;除非有独立的合理依据,否则应保留 断言和容差。
- 如果固定分辨率或几何尺寸本身就是约定,请保留明确的固定值。
- 分别验证录制和比较:检查预期的产物路径及有意进行的基准图差异。读取测试中的确切输出路径, 确认该产物存在,并在验证证据中注明该字面路径。当验证日志报告通过时,请说明通过了什么,然后 单独检查录制;比较通过并不能证明录制已写入产物。如果产物或基准图差异 不可用,请报告这一缺失,而不要声称录制成功。将特定工具的命令保留在仓库运行手册中。在仅报告发现的问题的审查中, 请说明缺少哪些证据;不要添加补救措施或重新运行步骤。
伪造图像和平台服务
当图像内容无关紧要时,伪造加载器,并在行为要求如此时断言请求的模型。具体钩子取决于你的图像库;项目辅助工具可能如下所示:
val requestedModels = mutableListOf<Any?>()
// Example helper, not a Compose API.
setContentWithFakeImageLoader { request ->
requestedModels += request.data
errorPainter()
}
当图像外观很重要时,请提供确定性的本地 painter/位图,而不是网络数据。