compose-ui-testing-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Compose: 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 proveTest shape
Text, button, loading/error branch, conditional contentPlain UI Compose test
Callback wiring from click/inputPlain UI Compose test
Focus navigation or keyboard behaviorCompose test with key input
Visual layout, clipping, elevation, typography, image compositionScreenshot test
State holder updates UI correctlyState holder/unit test plus one wiring smoke test
Navigation, lifecycle, DI integrationIntegration 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
runOnIdle
when the assertion needs Compose to finish applying snapshot state, recomposition, or queued UI work before reading the result.
使用简单计数器或捕获值:
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工作后才能读取结果时,使用
runOnIdle

Keyboard 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,
FocusRequester
, restoration, key handlers, and test patterns:
compose-focus-navigation
.
针对键盘、电视和桌面UI,使用与用户相同的输入模型(按键/方向键)来驱动导航,而不仅仅是点击。断言聚焦语义,而非颜色或缩放;截图仅用于验证视觉焦点样式。
详细信息——焦点图、
FocusRequester
、焦点恢复、按键处理器和测试模式:
compose-focus-navigation

Screenshot 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

常见错误

MistakeFix
Constructing full app graph to test an error rowTest plain UI with
state = Error
Testing click behavior through a ViewModel mockPass a callback and assert it was invoked
Screenshot test for simple text presenceUse semantics assertion
Semantics test for padding/color/focus ringUse screenshot test
Test tags everywherePrefer text/content description/role when stable
UI test depends on real image loading/network/timeFake or freeze the source
TV/keyboard UI tested with
performClick
only
Use key input and focus assertions; see compose-focus-navigation
错误做法修复方案
构建完整应用图来测试错误行使用
state = Error
测试纯UI
通过ViewModel mock测试点击行为传入回调并断言其被调用
用截图测试验证简单文本存在使用语义断言
用语义测试验证内边距/颜色/焦点环使用截图测试
到处使用测试标签当文本/内容描述/角色稳定时,优先使用它们
UI测试依赖真实图片加载/网络/时间模拟或冻结这些数据源
仅用
performClick
测试TV/键盘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来进行简单渲染。
  • 截图包含随机日期、时钟、远程图片或实时数据。
  • 断言仅检查执行操作后节点是否存在,而非回调/状态变化是否发生。
  • 焦点行为仅通过视觉检查,未进行断言。