launching-activities-with-activityscenario について
ActivityScenarioでActivityを起動し、ライフサイクルを制御するAndroid計装テスト向けのスキルガイドです。UIスレッドでの操作、再生成、結果の取得、ActivityScenarioRuleを扱います。
主な機能
- 01Activityの起動とライフサイクル制御
- 02設定変更を想定したActivityの再生成
- 03UIスレッドでのActivity操作
- 04Activity.RESULT_*値の取得
- 05ActivityScenarioRuleによるシナリオ管理
活用シーン
- 01非推奨のActivityTestRuleからの移行
- 02設定変更後の状態テスト
- 03Activityの戻り値コードの検証
- 04ActivityNotFoundExceptionやスレッドデッドロックの調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ skydoves/ android-testing-skills --skill launching-activities-with-activityscenarioインストール後、Claude Code で/launching-activities-with-activityscenarioを入力して読み込まれたか確認します。
詳細
ActivityScenario で Activity を起動する — ActivityTestRule を置き換える標準手段
ActivityScenario は、非推奨となった ActivityTestRule に代わる最新の手段です。スレッドセーフでライフサイクルを考慮したハンドルを提供し、Activity を CREATED/STARTED/RESUMED/DESTROYED の各状態へ遷移させたり、設定変更をエミュレートするために再作成したり、onActivity { } を介して UI スレッドに処理をポストしたりできます。JUnit4 ラッパーの ActivityScenarioRule は、before で launch を呼び出し、after で close を呼び出します。このスキルでは、標準的な FQN の落とし穴、スレッドに関するルール、マニフェスト要件を解説します。
このスキルを使う場面
- Activity をホストとして必要とする、新しいインストルメンテーションテストを作成する場合。
- 非推奨の
ActivityTestRuleからの移行で、置き換え方法を尋ねられた場合。 ActivityNotFoundExceptionでテストが失敗し、Activity がテスト APK のマニフェストに含まれていない場合。recreate()を使うテストで状態が「失われ」、rememberとrememberSaveableの違いをデバッグしている場合。- UI スレッドにアクションをポストしたところ、テストがデッドロックする場合(誤ったスレッドから
onActivityが呼び出されている)。 - テストで
setResult(...) + finish()を呼び出し、Activity.RESULT_*の値を読み取りたい場合。 - 再作成のテストに
Settings.Global.ALWAYS_FINISH_ACTIVITIES(「アクティビティを保持しない」)を使おうとしている場合 —ActivityScenarioはこれを拒否します。
このスキルを使わない場面
- ホストが Activity ではなく Fragment の場合 —
../launching-fragments-with-fragmentscenario/SKILL.mdを参照してください。 - ホストが Compose テスト用の
androidx.activity.ComponentActivityの場合 —createComposeRule()がすでにシナリオをラップしています。../../../compose/setup/configuring-test-dependencies/SKILL.mdを参照してください。 - ランナー/依存関係のスタックがまだセットアップされていない場合 —
../../runner/running-instrumented-tests-with-androidjunit4/SKILL.mdから始めてください。 - システム UI や別のアプリとやり取りする必要がある場合 —
../../uiautomator/cross-app-tests-with-uiautomator/SKILL.mdを参照してください。 - ホストテスト(Robolectric)とデバイステストのどちらを使うか選んでいる場合 —
../../../compose/setup/setting-up-host-vs-device-tests/SKILL.mdを参照してください。
前提条件
androidx.test.core.app.ActivityScenarioを使うためのandroidTestImplementation("androidx.test:core:1.7.0")。androidx.test.ext.junit.rules.ActivityScenarioRuleを使うためのandroidTestImplementation("androidx.test.ext:junit:1.3.0")。- ランナースキルに記載されている
androidTestImplementation("androidx.test:runner:1.7.0")とtestInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"。 - テスト対象の Activity を
src/androidTest/AndroidManifest.xml、src/debug/AndroidManifest.xml、または本番用マニフェストに宣言すること。
ワークフロー
- 1. 起動エントリーポイントを選びます。 4つすべてが
androidx.test.core.app.ActivityScenarioの静的ファクトリーです:
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.launchActivity // Kotlin reified extension
// Plain class-based launch
val s1: ActivityScenario<MyActivity> = ActivityScenario.launch(MyActivity::class.java)
// Reified inline (KTX)
val s2 = launchActivity<MyActivity>()
// With explicit intent (extras / data uri / action)
val intent = Intent(ApplicationProvider.getApplicationContext(), MyActivity::class.java)
.putExtra("user_id", 42)
val s3 = ActivityScenario.launch<MyActivity>(intent)
// For activities that call setResult(...) + finish() before reaching RESUMED
val s4 = ActivityScenario.launchActivityForResult(MyActivity::class.java)
launch は、Activity が RESUMED に到達するまで呼び出し元をブロックします。launchActivityForResult は、Activity が完了するまでブロックします。完了後に scenario.result を読み取ります。出典:androidx.test.core.app.ActivityScenario(tasks/research/R2-scenario.md の48~93行目)。
- 2.
moveToState(Lifecycle.State.*)でライフサイクルを操作します。 指定できる状態はCREATED、STARTED、RESUMED、DESTROYEDです。DESTROYEDは 終端状態 であり、一度到達するとシナリオを元に戻すことはできません(R2の100~102行目):
import androidx.lifecycle.Lifecycle
scenario.moveToState(Lifecycle.State.STARTED) // pause behind another activity
scenario.moveToState(Lifecycle.State.RESUMED) // back to foreground
scenario.moveToState(Lifecycle.State.DESTROYED) // terminal — close() is now a no-op
moveToState はメインスレッドから呼び出せません。@UiThreadTest メソッドや Looper ハンドラー内から呼び出すと、テストスレッドから計測バリアに対して await(...) するため、デッドロックが発生します(R2の823~828行目)。
- 3.
recreate()を使って構成変更をエミュレートします。 この処理ではonSaveInstanceStateを実行し、Activity を破棄した後、新しいインスタンスを作成してonRestoreInstanceStateを実行します。rememberSaveable/SavedStateHandle/Bundleを使って保存した状態は保持されます。通常のremember { ... }と単独のvarフィールドは保持 されません(R2の839~850行目):
scenario.recreate()
scenario.onActivity { activity ->
// activity is now a fresh instance
assertThat(activity.viewModel.count).isEqualTo(2) // survives if hoisted in the ViewModel
}
テストで recreate() 後も状態が保持されることを検証し、状態が失われていた場合、製品コードでは rememberSaveable ではなく remember を使っている可能性があります。
- 4.
onActivity { activity -> }内で Activity を読み取るか、変更します。 このブロックは UI スレッドで実行され、完了するまでテストスレッドをブロックします。フィールドやビューを直接操作するために認められている唯一の方法です:
scenario.onActivity { activity ->
activity.findViewById<EditText>(R.id.email).setText("[email protected]")
activity.viewModel.refresh()
}
activity 参照の有効範囲は ラムダ内に限られます。ラムダ本体の外で保持しないでください。recreate() によって無効になり、close() によってインスタンスが破棄されます。
スコープ外にデータを取り出すには、ホルダーを使います(R2の649~658行目):
var captured: MyActivity? = null
scenario.onActivity { captured = it }
val current: MyActivity = captured!!
- 5.
scenario.resultでActivity.RESULT_*を読み取ります。 これはlaunchActivityForResultで起動したシナリオでのみ有効であり、Activity がsetResult(...) + finish()を呼び出した後に限り読み取れます:
val scenario = ActivityScenario.launchActivityForResult(LoginActivity::class.java)
scenario.onActivity { it.confirmLogin() } // calls setResult(RESULT_OK) + finish()
val result: Instrumentation.ActivityResult = scenario.result
assertThat(result.resultCode).isEqualTo(Activity.RESULT_OK)
- 6. クラススコープの Activity には
ActivityScenarioRuleJUnit4 ラッパーを使います。 正規の FQN は1つだけで、androidx.test.ext.junit.rules.ActivityScenarioRuleです。androidx.test.ruleパッケージにはActivityScenarioRuleは含まれず、非推奨のActivityTestRuleのみが含まれます。移行時によくある混同です(R2の490行目):
import androidx.test.ext.junit.rules.ActivityScenarioRule
import org.junit.Rule
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@Test
fun loginButtonEnablesAfterEmail() {
activityRule.scenario.onActivity { it.email = "[email protected]" }
// ...
}
このルールは before で ActivityScenario.launch を呼び出し、after で scenario.close() を呼び出します。
- 7. Hilt と
@Rule(order = N)の順序を指定する場合は、Hilt を外側に配置します。 数字が小さいルールほど先に評価され、テストメソッドに近いルールほど後に実行されます:
@get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
- 8. テスト APK から参照できるマニフェストに、すべてのテスト Activity を宣言します。
ui-test-manifestアーティファクトが提供するのはandroidx.activity.ComponentActivityのみです。カスタムテスト Activity は、開発者がsrc/androidTest/AndroidManifest.xmlまたはsrc/debug/AndroidManifest.xmlで宣言する必要があります:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity android:name=".MyTestHostActivity" android:exported="true" />
</application>
</manifest>
宣言しない場合、本番環境の内容にかかわらず、ActivityScenario.launch は ActivityNotFoundException をスローします。Compose の同様の説明については ../../../compose/setup/configuring-test-dependencies/SKILL.md を参照してください。
- 9. シナリオを閉じます。
ActivityScenarioRuleを使う場合、JUnit が自動的にclose()を呼び出します。個別に起動する場合は、R2の138~150行目に従い、Kotlin のuse { }を使います(シナリオはjava.io.Closeableを実装しています):
ActivityScenario.launch<MyActivity>().use { scenario ->
scenario.onActivity { /* ... */ }
}
close() は現在の状態にかかわらず Activity を DESTROYED に遷移させます。また、べき等 なので、moveToState(DESTROYED) の後に呼び出しても安全です(何も起こりません)。
パターン
パターン:誤った例と正しい例 — ActivityTestRule(非推奨)と ActivityScenarioRule
// WRONG
import androidx.test.rule.ActivityTestRule
@Rule
public ActivityTestRule<MyActivity> rule = new ActivityTestRule<>(MyActivity.class);
// WRONG because: ActivityTestRule is @Deprecated at rules-1.7.0/.../ActivityTestRule.java:85.
// Its launch is racy with main-thread state, and lifecycle control is ad hoc.
// RIGHT
import androidx.test.ext.junit.rules.ActivityScenarioRule
@get:Rule
val rule = ActivityScenarioRule(MyActivity::class.java)
パターン:誤った例と正しい例 — ActivityScenarioRule のインポートパス(FQN の落とし穴)
// WRONG
import androidx.test.rule.ActivityScenarioRule // class does not exist!
// WRONG because: androidx.test:rules ships only the deprecated ActivityTestRule, not
// ActivityScenarioRule. The IDE auto-completes neither — the import is unresolved.
// RIGHT
import androidx.test.ext.junit.rules.ActivityScenarioRule
// Lives in androidx.test.ext:junit. This is the SINGLE canonical FQN.
パターン:誤った例と正しい例 — メインスレッドからの launch/moveToState の呼び出し
// WRONG
@Test @UiThreadTest
fun launchOnUiThread() {
val scenario = ActivityScenario.launch(MyActivity::class.java) // deadlocks
}
// WRONG because: launch / moveToState / onActivity / close all await an instrumentation
// barrier on the test thread. From the UI thread the barrier never fires (R2 lines 823-828).
// RIGHT
@Test
fun launchFromTestThread() {
val scenario = ActivityScenario.launch(MyActivity::class.java)
scenario.onActivity { activity ->
// UI-thread work goes here, NOT a recursive moveToState/launch
}
}
パターン:誤った例と正しい例 — ラムダの外での Activity 参照の保持
// WRONG
var leaked: MyActivity? = null
scenario.onActivity { leaked = it }
scenario.recreate()
leaked!!.someField // stale reference; underlying instance is gone
// RIGHT
scenario.onActivity { activity ->
activity.someField = "new value"
}
// or: re-acquire after recreate()
scenario.recreate()
scenario.onActivity { activity ->
assertThat(activity.someField).isEqualTo("new value (restored from saved state)")
}
パターン:誤りと正しい方法 — 再作成テストで「アクティビティを保持しない」に頼る
// WRONG
// Toggle Settings.Global.ALWAYS_FINISH_ACTIVITIES to force recreation between activities.
// WRONG because: ActivityScenario does not honor "Don't keep activities" — its barrier
// model expects deterministic transitions. Use scenario.recreate() instead.
// RIGHT
scenario.recreate() // deterministic onSaveInstanceState → destroy → create → onRestoreInstanceState
必須ルール
- 必須:
androidx.test.ext.junit.rulesからActivityScenarioRuleをインポートしてください。パッケージandroidx.test.ruleには含まれていません。 - 禁止: 新しいテストで
ActivityTestRuleを使用しないでください。これは@Deprecatedです。Lint とコードモッドによって置き換えられます。 - 禁止: メインスレッドから
ActivityScenario.launch、moveToState、closeを呼び出さないでください。これらは、UI スレッド以外でのみ発生するインストルメンテーションバリアでブロックされます。 - 禁止:
onActivityが返すactivity参照をラムダ本体の外で保持しないでください。recreate()とclose()により無効になります。 - 禁止: 再作成を強制するために
Settings.Global.ALWAYS_FINISH_ACTIVITIESに頼らないでください。ActivityScenarioはこれを拒否します。scenario.recreate()を使用してください。 - 必須: カスタムテストアクティビティを
src/androidTest/AndroidManifest.xmlまたはsrc/debug/AndroidManifest.xmlで宣言してください。ui-test-manifestアーティファクトが追加するのはandroidx.activity.ComponentActivityのみです。 - 必須:
moveToState(Lifecycle.State.DESTROYED)またはclose()を呼び出してください(あるいは自動的に実行するActivityScenarioRuleを使用してください)。リークがあると後続のテストに影響します。 - 推奨: クラス単位でスコープを設定する Activity テストでは
ActivityScenarioRuleを使用してください。起動のタイミングを細かく制御する必要があるテストではActivityScenario.launch(...).use { }を使用してください。 - 推奨: Hilt の
@get:Rule(order = 0)、ActivityScenarioRuleのorder = 1(またはそれ以上)という順序にしてください。これにより Activity の起動前に Hilt がインストールされます。
検証
-
grep -r "androidx.test.rule.ActivityScenarioRule" src/が何も返さないこと。インポートするのはandroidx.test.ext.junit.rules.ActivityScenarioRuleのみです。 - 新しいコードで
grep -r "ActivityTestRule" src/が何も返さないこと。 - すべてのカスタムテスト Activity が
src/androidTest/AndroidManifest.xmlまたはsrc/debug/AndroidManifest.xmlで宣言されていること。 -
scenario.recreate()を呼び出すテストでは、保持する必要のある状態にrememberSaveable、SavedStateHandle、onSaveInstanceStateのいずれかに基づく状態管理を使用していること。 -
@UiThreadTestメソッドからlaunch、moveToState、closeを直接呼び出していないこと。 -
./gradlew :<module>:connectedDebugAndroidTestが、ActivityNotFoundExceptionやCannot run onActivity since Activity is not aliveを発生させずに実行されること。 -
Activity.RESULT_*を検証するテストでは、通常のlaunchではなくlaunchActivityForResultを使用していること。
参考資料
- Android Developers — アプリのアクティビティをテストする(ActivityScenario):https://developer.android.com/guide/components/activities/testing
- Android Developers — AndroidX Test ガイド:https://developer.android.com/training/testing/instrumented-tests/androidx-test-libraries/rules
- AndroidX Test リリースノート:https://developer.android.com/jetpack/androidx/releases/test
androidx.test.core.app.ActivityScenario—launch、launchActivityForResult、moveToState、recreate、onActivity、state、result、close。tasks/research/R2-scenario.mdの 48~160 行を参照してください。junit-1.3.0/androidx/test/ext/junit/rules/ActivityScenarioRule.java— 標準的な JUnit4 ラッパーです。rules-1.7.0/androidx/test/rule/ActivityTestRule.javaの 85~86 行 —@Deprecatedです。非推奨を示す Javadoc では、明示的にActivityScenarioの使用を案内しています。tasks/research/R2-scenario.md— ActivityScenario と FragmentScenario の詳細レポートです。813~919 行では、よくある落とし穴(メインスレッドからの起動、終端状態である DESTROYED、recreate()とrememberの組み合わせなど)をすべて列挙しています。docs/CORPUS.mdセクション H.3 — ActivityScenario API と JUnit4 ラッパーの FQN。tasks/research/R1-androidx-test-core.mdの 477~535 行 — 2 種類のActivityScenarioRuleの混同と、標準的な FQN に関する説明です。