testing-with-espresso-interop について
Android の同じ Activity で Jetpack Compose と Espresso を併用するテストスキルです。同期やテストスレッドのルールに加え、Android View、ダイアログ、ソフトウェアキーボードとの操作を扱います。
主な機能
- 01Compose と Espresso API の併用
- 02Compose と Espresso のアイドル状態連携
- 03Android ダイアログと IME の操作
- 04Compose ホストへのファインダーの限定
活用シーン
- 01Compose テストで Android ダイアログを操作
- 02テストでソフトウェアキーボードを検証
- 03View と Compose の混在画面をテスト
- 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-84onRootWithViewInteractionの定義:compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.android.ktcreateAndroidComposeRulev2: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