| name | launching-activities-with-activityscenario |
| description | Use this skill to launch, drive, and tear down an Activity from an instrumentation test using `ActivityScenario` and the JUnit4 wrapper `ActivityScenarioRule`. Covers `ActivityScenario.launch<A>()`, `launch(intent)`, `launchActivityForResult`, `moveToState(Lifecycle.State.*)`, `recreate()`, `onActivity { }`, the `Closeable` / `use { }` idiom, and the manifest declaration trap. Establishes that `androidx.test.ext.junit.rules.ActivityScenarioRule` is the single canonical FQN — `androidx.test.rule.ActivityScenarioRule` does NOT exist; the package only ships the deprecated `ActivityTestRule`. Use when the user reports `ActivityNotFoundException`, `ActivityTestRule deprecated`, "how do I rotate the activity in a test", "how do I read setResult from a test", "how do I survive recreate", "Cannot run onActivity since Activity is not alive", or "test deadlocks on launch". |
| license | Apache-2.0. See LICENSE for complete terms. |
| metadata | {"author":"Jaewoong Eum (skydoves)","keywords":["android-instrumentation-test","ActivityScenario","ActivityScenarioRule","ActivityTestRule-deprecated","moveToState","recreate","onActivity","launchActivityForResult","Lifecycle-State","configuration-change","Closeable"]} |
Launching Activities with ActivityScenario — The Canonical ActivityTestRule Replacement
ActivityScenario is the modern replacement for the deprecated ActivityTestRule. It provides a thread-safe, lifecycle-aware handle to drive an Activity through CREATED / STARTED / RESUMED / DESTROYED, recreate it for configuration-change emulation, and post work onto its UI thread via onActivity { }. The JUnit4 wrapper ActivityScenarioRule calls launch in before and close in after. This skill encodes the canonical FQN trap, the threading rules, and the manifest requirement.
When to use this skill
- The user is starting a new instrumentation test that needs an Activity host.
- The user is migrating from
ActivityTestRule (deprecated) and asks for the replacement.
- A test fails with
ActivityNotFoundException and the Activity is missing from the test APK manifest.
- A
recreate() test "loses" state and the user is debugging remember vs rememberSaveable.
- The user posts an action onto the UI thread and the test deadlocks (
onActivity called from the wrong thread).
- A test calls
setResult(...) + finish() and wants to read the Activity.RESULT_* back.
- The user mentions
Settings.Global.ALWAYS_FINISH_ACTIVITIES ("Don't keep activities") as a way to test recreation — ActivityScenario rejects it.
When NOT to use this skill
- The host is a Fragment, not an Activity — see
../launching-fragments-with-fragmentscenario/SKILL.md.
- The host is
androidx.activity.ComponentActivity for Compose tests — createComposeRule() already wraps the scenario; see ../../../compose/setup/configuring-test-dependencies/SKILL.md.
- The runner / dependency stack is not yet set up — start with
../../runner/running-instrumented-tests-with-androidjunit4/SKILL.md.
- The user needs to interact with system UI or another app — see
../../uiautomator/cross-app-tests-with-uiautomator/SKILL.md.
- The user is choosing between host (Robolectric) and device tests — see
../../../compose/setup/setting-up-host-vs-device-tests/SKILL.md.
Prerequisites
androidTestImplementation("androidx.test:core:1.7.0") for androidx.test.core.app.ActivityScenario.
androidTestImplementation("androidx.test.ext:junit:1.3.0") for androidx.test.ext.junit.rules.ActivityScenarioRule.
androidTestImplementation("androidx.test:runner:1.7.0") and testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" from the runner skill.
- The test Activity declared in
src/androidTest/AndroidManifest.xml, src/debug/AndroidManifest.xml, or the production manifest.
Workflow
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.launchActivity
val s1: ActivityScenario<MyActivity> = ActivityScenario.launch(MyActivity::class.java)
val s2 = launchActivity<MyActivity>()
val intent = Intent(ApplicationProvider.getApplicationContext(), MyActivity::class.java)
.putExtra("user_id", 42)
val s3 = ActivityScenario.launch<MyActivity>(intent)
val s4 = ActivityScenario.launchActivityForResult(MyActivity::class.java)
launch blocks the caller until the Activity reaches RESUMED. launchActivityForResult blocks until the Activity completes — read scenario.result afterwards. Source: androidx.test.core.app.ActivityScenario (per tasks/research/R2-scenario.md lines 48-93).
import androidx.lifecycle.Lifecycle
scenario.moveToState(Lifecycle.State.STARTED)
scenario.moveToState(Lifecycle.State.RESUMED)
scenario.moveToState(Lifecycle.State.DESTROYED)
moveToState cannot be called from the main thread; calling from a @UiThreadTest method or from inside a Looper handler deadlocks because it await(...)s an instrumentation barrier from the test thread (R2 lines 823-828).
scenario.recreate()
scenario.onActivity { activity ->
assertThat(activity.viewModel.count).isEqualTo(2)
}
If a test asserts that state survives recreate() and finds it gone, the production code probably uses remember, not rememberSaveable.
scenario.onActivity { activity ->
activity.findViewById<EditText>(R.id.email).setText("test@example.com")
activity.viewModel.refresh()
}
The activity reference is scoped to the lambda. Never store it past the lambda body — recreate() invalidates it, and close() destroys the instance.
To capture data out of the scope, use a holder (R2 lines 649-658):
var captured: MyActivity? = null
scenario.onActivity { captured = it }
val current: MyActivity = captured!!
val scenario = ActivityScenario.launchActivityForResult(LoginActivity::class.java)
scenario.onActivity { it.confirmLogin() }
val result: Instrumentation.ActivityResult = scenario.result
assertThat(result.resultCode).isEqualTo(Activity.RESULT_OK)
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 = "x@y.z" }
}
The rule calls ActivityScenario.launch in before and scenario.close() in after.
@get:Rule(order = 0) val hiltRule = HiltAndroidRule(this)
@get:Rule(order = 1) val activityRule = ActivityScenarioRule(LoginActivity::class.java)
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity android:name=".MyTestHostActivity" android:exported="true" />
</application>
</manifest>
Without this, ActivityScenario.launch throws ActivityNotFoundException regardless of what is in production. See ../../../compose/setup/configuring-test-dependencies/SKILL.md for the parallel Compose story.
ActivityScenario.launch<MyActivity>().use { scenario ->
scenario.onActivity { }
}
close() drives the activity to DESTROYED regardless of current state and is idempotent — safe to call after moveToState(DESTROYED) (no-op).
Patterns
Pattern: WRONG vs RIGHT — ActivityTestRule (deprecated) vs ActivityScenarioRule
import androidx.test.rule.ActivityTestRule
@Rule
public ActivityTestRule<MyActivity> rule = new ActivityTestRule<>(MyActivity.class);
import androidx.test.ext.junit.rules.ActivityScenarioRule
@get:Rule
val rule = ActivityScenarioRule(MyActivity::class.java)
Pattern: WRONG vs RIGHT — ActivityScenarioRule import path (the FQN trap)
import androidx.test.rule.ActivityScenarioRule
import androidx.test.ext.junit.rules.ActivityScenarioRule
Pattern: WRONG vs RIGHT — calling launch / moveToState from the main thread
@Test @UiThreadTest
fun launchOnUiThread() {
val scenario = ActivityScenario.launch(MyActivity::class.java)
}
@Test
fun launchFromTestThread() {
val scenario = ActivityScenario.launch(MyActivity::class.java)
scenario.onActivity { activity ->
}
}
Pattern: WRONG vs RIGHT — capturing the activity reference past the lambda
var leaked: MyActivity? = null
scenario.onActivity { leaked = it }
scenario.recreate()
leaked!!.someField
scenario.onActivity { activity ->
activity.someField = "new value"
}
scenario.recreate()
scenario.onActivity { activity ->
assertThat(activity.someField).isEqualTo("new value (restored from saved state)")
}
Pattern: WRONG vs RIGHT — relying on "Don't keep activities" for recreate testing
scenario.recreate()
Mandatory rules
- MUST import
ActivityScenarioRule from androidx.test.ext.junit.rules. The package androidx.test.rule does NOT contain it.
- MUST NOT use
ActivityTestRule for new tests — it is @Deprecated. The lint and codemods replace it.
- MUST NOT call
ActivityScenario.launch, moveToState, or close from the main thread. They block on an instrumentation barrier that only fires off the UI thread.
- MUST NOT store the
activity reference returned by onActivity past the lambda body. recreate() and close() invalidate it.
- MUST NOT rely on
Settings.Global.ALWAYS_FINISH_ACTIVITIES to force recreation — ActivityScenario rejects it. Use scenario.recreate().
- MUST declare custom test activities in
src/androidTest/AndroidManifest.xml or src/debug/AndroidManifest.xml. The ui-test-manifest artifact only contributes androidx.activity.ComponentActivity.
- MUST call
moveToState(Lifecycle.State.DESTROYED) or close() (or use ActivityScenarioRule, which does it automatically) — leaks confuse subsequent tests.
- PREFERRED: use
ActivityScenarioRule for class-scoped Activity tests; use ActivityScenario.launch(...).use { } for tests that need fine-grained launch timing.
- PREFERRED: order
@get:Rule(order = 0) for Hilt and order = 1 (or higher) for ActivityScenarioRule so Hilt installs before the Activity is launched.
Verification
References
- Android Developers — Test your app's activities (ActivityScenario): https://developer.android.com/guide/components/activities/testing
- Android Developers — AndroidX Test guide: https://developer.android.com/training/testing/instrumented-tests/androidx-test-libraries/rules
- AndroidX Test release notes: https://developer.android.com/jetpack/androidx/releases/test
androidx.test.core.app.ActivityScenario — launch, launchActivityForResult, moveToState, recreate, onActivity, state, result, close. See tasks/research/R2-scenario.md lines 48-160.
junit-1.3.0/androidx/test/ext/junit/rules/ActivityScenarioRule.java — the canonical JUnit4 wrapper.
rules-1.7.0/androidx/test/rule/ActivityTestRule.java lines 85-86 — @Deprecated; the deprecation Javadoc explicitly redirects to ActivityScenario.
tasks/research/R2-scenario.md — full ActivityScenario + FragmentScenario report. Lines 813-919 enumerate every common pitfall (main-thread launch, DESTROYED terminal, recreate() + remember, etc.).
docs/CORPUS.md Section H.3 — ActivityScenario API surface and JUnit4 wrapper FQN.
tasks/research/R1-androidx-test-core.md lines 477-535 — the dual-ActivityScenarioRule confusion and the canonical FQN.