mcpField
T
testing-with-espresso-interop

testing-with-espresso-interop

github.com/skydoves测试与质量33959 次安装22 周前更新
代码仓库

关于 testing-with-espresso-interop

这项测试技能介绍如何在同一个 Android Activity 中结合 Jetpack Compose 与 Espresso。重点涵盖同步、测试线程规则,以及对 Android View、对话框和软键盘的交互。

主要功能

  1. 01组合使用 Compose 与 Espresso API
  2. 02桥接 Compose 与 Espresso 空闲状态
  3. 03交互 Android 对话框和输入法
  4. 04将查找器限定到 Compose 宿主

适用场景

  1. 01在 Compose 测试中操作 Android 对话框
  2. 02验证测试中的软键盘行为
  3. 03测试 View 与 Compose 混合界面
  4. 04排查在 UI 线程调用 Espresso 的问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/skydoves/android-testing-skills --skill testing-with-espresso-interop

安装后在 Claude Code 中输入/testing-with-espresso-interop确认已加载。

详细介绍

使用 Espresso 互操作测试——一个 Activity、两个测试框架、一个桥接器

Compose ComposeTestRule 和 Espresso onView API 可同时操作同一个 Activity。Compose 同步机制(空闲资源、等待帧时钟)通过名为 EspressoLink 的单个内部 IdlingResource 桥接到 Espresso,因此开发者无需手动注册任何内容。关键在于线程:Espresso.onView 必须在测试线程上运行,不能在 rule.runOnIdle { } 或 rule.runOnUiThread { } 内调用。本技能收录了 androidx.compose.foundation 文本框 IME 测试中的规范互操作模式。

何时使用此技能

  • 测试必须操作 Android Dialog 窗口(它位于独立的 Window 中,可能不在 Compose 语义树内)。
  • 测试必须验证 IME(软键盘)状态——只有 Espresso 的 onView(supportsInputMethods()).perform(click()) 才会显示键盘。
  • 被测屏幕是混合型 Activity,其 Android View 层级中包含 ComposeView,反之亦然。
  • 开发者询问“如何在同一个测试中使用 Espresso 和 Compose”。
  • 测试在 runOnIdle { } 中调用 Espresso.onView(...),随后卡住或抛出“cannot be run from the main thread”。

何时不要使用此技能

  • 测试完全使用 Compose;不存在 Android View 交互。请使用 ../../patterns/structuring-a-compose-test/SKILL.md。
  • 同步问题是“测试在本地通过,但在 CI 上不稳定”,且没有任何 View 互操作。请使用 ../../synchronization/synchronizing-with-idle/SKILL.md。
  • 测试针对 androidx.compose.ui.window.Dialog(Compose 对话框)——它确实会通过 isDialog() 出现在语义树中,无需使用 Espresso。请使用 ../../finders/composing-semantics-matchers/SKILL.md。

先决条件

  • 在 androidTestImplementation 中添加 androidx.test.espresso:espresso-core。
  • 在 androidTestImplementation 中添加 androidx.compose.ui:ui-test-junit4,并在 debugImplementation 中添加 androidx.compose.ui:ui-test-manifest(参见 ../../setup/configuring-test-dependencies/SKILL.md)。
  • 一个承载这两个层的 Activity——通常选用 FragmentActivity;在 src/androidTest/AndroidManifest.xml 中声明自定义 Activity 也可行。
  • 来自 ../../patterns/structuring-a-compose-test/SKILL.md 的测试类框架。

工作流

  • 1. 使用 createAndroidComposeRule<A>()(v2)作为宿主 Activity。 引自 compose/foundation/foundation/src/androidDeviceTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusCustomDialogTest.kt:60:
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
import androidx.fragment.app.FragmentActivity
import kotlinx.coroutines.test.StandardTestDispatcher

@get:Rule
val rule = createAndroidComposeRule<FragmentActivity>(StandardTestDispatcher())
  • 2. 使用 rule.setContent { } 设置 Compose 内容,然后调用 rule.waitForIdle()。 这会清空 Compose recomposer/effect 队列,确保后续 Espresso 交互看到稳定的视图树。

  • 3. 在测试线程上通过 Espresso.onView(...) 操作 View 层。 不要将此调用包装在 runOnIdle 或 runOnUiThread 中。

import androidx.test.espresso.Espresso
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.matcher.ViewMatchers

rule.waitForIdle()
Espresso.onView(ViewMatchers.supportsInputMethods()).perform(ViewActions.click())

引自 TextFieldFocusCustomDialogTest.kt:117-119。完整的 IME 测试:

@Test
fun keyboardShown_forFieldInAndroidDialog_…() {
    val focusRequester = FocusRequester()
    val keyboardHelper = KeyboardHelper(rule)

    rule.setContent {
        wrapContent {
            keyboardHelper.initialize()
            LaunchedEffect(Unit) { focusRequester.requestFocus() }
            BasicTextField(
                value = "",
                onValueChange = {},
                modifier = Modifier.focusRequester(focusRequester),
            )
        }
    }

    rule.waitForIdle()
    // The dialog's window must be focused for the IME to actually show.
    Espresso.onView(ViewMatchers.supportsInputMethods()).perform(ViewActions.click())

    keyboardHelper.waitForKeyboardVisibility(visible = true)
}
  • 4. 在测试线程上自由混用 Compose 和 Espresso 断言。 这两个 API 操作的是同一个 Activity。
rule.onNodeWithTag("submit").performClick()
Espresso.onView(ViewMatchers.withId(R.id.legacy_toast)).check(matches(ViewMatchers.isDisplayed()))
  • 5.(可选)将 Compose 查找器限定到由 Espresso 解析出的子树。 当 Activity 包含多个 Compose 宿主,且单个 Espresso ViewInteraction 可确定要与哪个宿主交互时,使用 onRootWithViewInteraction(viewInteraction)。此功能仅受 AndroidComposeTestRule 支持;非 Android 测试规则会抛出 "This implementation of ComposeTestRule does not support onRootWithViewInteraction."。
val nestedComposeView = Espresso.onView(ViewMatchers.withId(R.id.nested_compose))
rule.onRootWithViewInteraction(nestedComposeView).onNodeWithTag("submit").performClick()
  • 6. 信任 EspressoLink 桥接机制。 Compose 的 IdlingResource——recomposer、snapshot、frame clock——由 ComposeIdlingResource 汇总,并通过名为 "Compose-Espresso link" 的单个 IdlingResource 提供给 Espresso。引自 compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/EspressoLink.android.kt:34-84。框架会在 withStrategy { } 中注册和注销该资源(第 58-73 行)。开发者无需为 Compose 状态调用 IdlingRegistry.getInstance().register(...)。

  • 7. 反过来,Compose 也能感知 Espresso 的 idling resource。 通过 IdlingRegistry.getInstance().register(…) 注册的任何资源,都会被 rule.waitForIdle() 等待,因为测试规则会通过同一桥接机制轮询 Espresso。

模式

模式:从 runOnIdle 调用 Espresso.onView 会导致死锁
// WRONG
@Test
fun submit() {
    rule.setContent { /* Compose UI with a button that opens a Dialog */ }
    rule.onNodeWithTag("open").performClick()
    rule.runOnIdle {
        Espresso.onView(ViewMatchers.withId(R.id.confirm)).perform(ViewActions.click())
    }
}
// WRONG because: runOnIdle posts to the UI thread. Espresso.onView calls Espresso.onIdle()
// internally, and EspressoLink.runUntilIdle (EspressoLink.android.kt:75-83) explicitly
// throws on UI-thread invocations:
//   "Functions that involve synchronization (Assertions, Actions, Synchronization;
//    e.g. assertIsSelected(), doClick(), runOnIdle()) cannot be run from the main thread.
//    Did you nest such a function inside runOnIdle {}, runOnUiThread {} or setContent {}?"
// RIGHT
@Test
fun submit() {
    rule.setContent { /* Compose UI with a button that opens a Dialog */ }
    rule.onNodeWithTag("open").performClick()
    rule.waitForIdle()
    Espresso.onView(ViewMatchers.withId(R.id.confirm)).perform(ViewActions.click())
}
模式:通过 Espresso 设置对话框焦点,通过 Compose 操作内容
// WRONG — assuming Compose's IME helper alone is enough
@Test
fun dialogKeyboard() {
    rule.setContent { CustomDialog { BasicTextField(/* … */) } }
    keyboardHelper.waitForKeyboardVisibility(visible = true)   // never visible
}
// WRONG because: Android Dialogs live in a separate Window. Even if the TextField requests
// focus, the soft keyboard does not appear until the dialog window itself takes input
// focus. Only Espresso can drive that — `onView(supportsInputMethods()).perform(click())`.
// RIGHT
@Test
fun dialogKeyboard() {
    rule.setContent { CustomDialog { BasicTextField(/* … */) } }
    rule.waitForIdle()
    Espresso.onView(ViewMatchers.supportsInputMethods()).perform(ViewActions.click())
    keyboardHelper.waitForKeyboardVisibility(visible = true)
}
模式:createComposeRule() 无法访问 Activity
// WRONG
@get:Rule val rule = createComposeRule(StandardTestDispatcher())     // ComponentActivity host
@Test fun customActivityFlow() {
    Espresso.onView(ViewMatchers.withId(R.id.my_activity_view))      // does not exist
        .perform(ViewActions.click())
}
// WRONG because: createComposeRule launches the empty ComponentActivity from ui-test-manifest.
// To interact with a custom Activity's view hierarchy, launch that Activity via
// createAndroidComposeRule<MyActivity>().
// RIGHT
@get:Rule val rule = createAndroidComposeRule<MyActivity>(StandardTestDispatcher())

强制规则

  • 必须在测试线程上、调用 rule.waitForIdle() 之后调用 Espresso.onView(...)。不得将其包装在 rule.runOnIdle { } 或 rule.runOnUiThread { } 中——Espresso 的空闲等待在 UI 线程上会抛出异常(EspressoLink.android.kt:75-83)。
  • 必须在任何需要访问宿主 Activity 的 View ID 的测试中使用 createAndroidComposeRule<A>(StandardTestDispatcher())。createComposeRule() 只会启动来自 ui-test-manifest 的基础 ComponentActivity。
  • 不得手动通过 IdlingRegistry.getInstance() 注册 Compose IdlingResource——框架会通过 EspressoLink(名为 "Compose-Espresso link" 的单个桥接资源)完成此操作。引用见 EspressoLink.android.kt:34-84。
  • **优先:**当 Activity 承载多个 ComposeView 时,通过 rule.onRootWithViewInteraction(viewInteraction) 将 Compose 交互限定到 Compose 子树。在非 AndroidComposeTestRule 实现上调用该方法会抛出 IllegalStateException(源码在 ComposeTestRuleExt.android.kt:38-42 中使用 error("This implementation of ComposeTestRule does not support onRootWithViewInteraction."))。
  • **优先:**对于软键盘测试,遵循 rule.setContent { … } → rule.waitForIdle() → Espresso.onView(supportsInputMethods()).perform(click()) → keyboardHelper.waitForKeyboardVisibility(visible = true)(TextFieldFocusCustomDialogTest.kt:117-128)。
  • 不得将此技能用于 androidx.compose.ui.window.Dialog(Compose 对话框)——这类对话框会作为由 isDialog() 匹配的语义节点呈现。请参阅 ../../finders/composing-semantics-matchers/SKILL.md。

验证

  • 规则为 createAndroidComposeRule<HostActivity>(StandardTestDispatcher()),而不是 createComposeRule()。
  • 每个 Espresso.onView(...) 调用都位于测试方法作用域内,而不是位于 runOnIdle { } / runOnUiThread { } / setContent { } 内。
  • 在 Compose 操作与下一次 Espresso.onView(...) 调用之间(或反过来)调用 rule.waitForIdle()。
  • 不要手动调用 IdlingRegistry.getInstance().register(composeIdlingResource)——框架会通过 EspressoLink 完成此操作。
  • 当一个 Activity 中存在多个 ComposeView 时,通过 rule.onRootWithViewInteraction(...) 限定查找器的作用范围。
  • 不要使用 Thread.sleep 来“等待 Espresso 稳定下来”——rule.waitForIdle() 已经会通过桥接轮询 Espresso。请参阅 ../../synchronization/synchronizing-with-idle/SKILL.md。

参考资料

  • Espresso onView API:https://developer.android.com/training/testing/espresso/basics
  • Compose-Espresso 互操作指南:https://developer.android.com/develop/ui/compose/testing#interop-uiautomator-espresso
  • IME 互操作规范测试:compose/foundation/foundation/src/androidDeviceTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusCustomDialogTest.kt:57-128
  • EspressoLink 桥接实现:compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/EspressoLink.android.kt:34-84
  • onRootWithViewInteraction 定义:compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.kt
  • createAndroidComposeRule v2:compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/v2/AndroidComposeTestRule.android.kt
  • KeyboardHelper 工具:compose/foundation/foundation/src/androidDeviceTest/kotlin/androidx/compose/foundation/text/KeyboardHelper.kt

评论

支持 Markdown