mcpField
L
launching-activities-with-activityscenario

launching-activities-with-activityscenario

github.com/skydovesテスト・品質33962 回インストール22 週間前に更新
リポジトリ

launching-activities-with-activityscenario について

ActivityScenarioでActivityを起動し、ライフサイクルを制御するAndroid計装テスト向けのスキルガイドです。UIスレッドでの操作、再生成、結果の取得、ActivityScenarioRuleを扱います。

主な機能

  1. 01Activityの起動とライフサイクル制御
  2. 02設定変更を想定したActivityの再生成
  3. 03UIスレッドでのActivity操作
  4. 04Activity.RESULT_*値の取得
  5. 05ActivityScenarioRuleによるシナリオ管理

活用シーン

  1. 01非推奨のActivityTestRuleからの移行
  2. 02設定変更後の状態テスト
  3. 03Activityの戻り値コードの検証
  4. 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 には ActivityScenarioRule JUnit4 ラッパーを使います。 正規の 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 に関する説明です。

コメント