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. 01Compose と Espresso API の併用
  2. 02Compose と Espresso のアイドル状態連携
  3. 03Android ダイアログと IME の操作
  4. 04Compose ホストへのファインダーの限定

活用シーン

  1. 01Compose テストで Android ダイアログを操作
  2. 02テストでソフトウェアキーボードを検証
  3. 03View と Compose の混在画面をテスト
  4. 04UI スレッドでの Espresso 呼び出しを調査

インストール

コマンドをコピーしてターミナルで実行します。

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

インストール後、Claude Code で/testing-with-espresso-interopを入力して読み込まれたか確認します。

詳細

Espresso Interop によるテスト — 1 つの Activity、2 つのテストフレームワーク、1 つのブリッジ

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()) だけです。
  • テスト対象の画面が、ComposeView を含む Android View 階層を持つハイブリッドな Activity である、またはその逆である。
  • 開発者が「同じテストで Espresso と Compose を使う方法」を尋ねている。
  • テストが runOnIdle { } から Espresso.onView(...) を呼び出してハングする、または「メインスレッドから実行できない」という例外をスローする。

このスキルを使わない場面

  • テストが純粋な 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 を参照)。
  • 2 つのレイヤーをホストする Activity があること。標準的な選択肢は FragmentActivity です。src/androidTest/AndroidManifest.xml で宣言されたカスタム Activity も使用できます。
  • ../../patterns/structuring-a-compose-test/SKILL.md にあるテストクラスのスケルトンがあること。

ワークフロー

  • 1. ホスト Activity には createAndroidComposeRule<A>()(v2)を使用します。 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 のアサーションを自由に組み合わせられます。 2 つの API は同じ Activity に対して実行されます。
rule.onNodeWithTag("submit").performClick()
Espresso.onView(ViewMatchers.withId(R.id.legacy_toast)).check(matches(ViewMatchers.isDisplayed()))
  • 5.(任意)Espresso で解決したサブツリーに Compose のファインダーをスコープします。 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. 逆に、Espresso のアイドリングリソースは Compose からも参照できます。 IdlingRegistry.getInstance().register(…) で登録されたものは、ルールが同じブリッジを通じて Espresso をポーリングするため、rule.waitForIdle() によって待機されます。

パターン

パターン: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 のみです。
  • 禁止: Compose の IdlingResource を IdlingRegistry.getInstance() に手動で登録しないでください。フレームワークでは 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 を通じて処理します。
  • 1 つの Activity に複数の ComposeView がある場合、rule.onRootWithViewInteraction(...) を使ってファインダーの対象を限定しています。
  • 「Espresso が落ち着くのを待つ」ために Thread.sleep を使用していません。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

コメント