关于 launching-activities-with-activityscenario
面向 Android 仪器测试的技能指南,说明如何用 ActivityScenario 启动 Activity 并控制其生命周期。内容涵盖 UI 线程访问、配置变更重建、结果读取和 ActivityScenarioRule 的使用。
主要功能
- 01启动并控制 Activity 生命周期
- 02通过 recreate 模拟配置变更
- 03在 UI 线程读取或修改 Activity
- 04读取 Activity.RESULT_* 返回值
- 05使用 ActivityScenarioRule 自动管理场景
适用场景
- 01替换已弃用的 ActivityTestRule
- 02测试旋转等配置变更后的状态
- 03验证 Activity 返回结果
- 04排查 ActivityNotFoundException 或线程死锁
安装
复制下面的命令,在终端中运行。
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 状态间切换,为模拟配置变更而重新创建 Activity,并通过 onActivity { } 将工作提交到其 UI 线程。JUnit4 包装类 ActivityScenarioRule 会在 before 中调用 launch,并在 after 中调用 close。本技能涵盖标准 FQN 陷阱、线程规则和清单要求。
适用场景
- 用户正在编写需要 Activity 宿主的新插桩测试。
- 用户正在从已弃用的
ActivityTestRule迁移,并询问替代方案。 - 测试因
ActivityNotFoundException失败,且测试 APK 清单中缺少该 Activity。 recreate()测试“丢失”了状态,用户正在排查remember与rememberSaveable的区别。- 用户向 UI 线程提交操作后,测试发生死锁(从错误的线程调用了
onActivity)。 - 测试调用
setResult(...) + finish(),并希望读取返回的Activity.RESULT_*。 - 用户提到
Settings.Global.ALWAYS_FINISH_ACTIVITIES(“不保留活动”)作为测试重建的方法——ActivityScenario不支持这种方式。
不适用场景
- 宿主是 Fragment,而不是 Activity——请参阅
../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。
前置条件
- 通过
androidTestImplementation("androidx.test:core:1.7.0")添加androidx.test.core.app.ActivityScenario。 - 通过
androidTestImplementation("androidx.test.ext:junit:1.3.0")添加androidx.test.ext.junit.rules.ActivityScenarioRule。 - 根据运行器技能,添加
androidTestImplementation("androidx.test:runner:1.7.0"),并设置testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"。 - 在
src/androidTest/AndroidManifest.xml、src/debug/AndroidManifest.xml或生产清单中声明测试 Activity。
工作流
- 1. 选择启动入口。 这四个都是
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是终态——到达后,scenario 无法再返回(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(...) 的 instrumentation 屏障(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() 后仍然存在,却发现状态消失了,生产代码很可能使用的是 remember,而不是 rememberSaveable。
- 4. 在
onActivity { activity -> }内读取或修改 Activity。 此代码块会在 UI 线程上运行,并阻塞测试线程直到执行完成;这是直接访问字段/视图的唯一合法方式:
scenario.onActivity { activity ->
activity.findViewById<EditText>(R.id.email).setText("[email protected]")
activity.viewModel.refresh()
}
activity 引用的作用域仅限于 lambda。绝不要在 lambda 代码块之外保存该引用——recreate() 会使其失效,而 close() 会销毁此实例。
若要将数据从作用域中取出,请使用 holder(R2 第 649-658 行):
var captured: MyActivity? = null
scenario.onActivity { captured = it }
val current: MyActivity = captured!!
- 5. 通过
scenario.result读取Activity.RESULT_*。 仅适用于使用launchActivityForResult启动的 scenario,并且只有在 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 是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 可见的 manifest 中声明每个测试 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. 关闭 scenario。 使用
ActivityScenarioRule时,JUnit 会自动调用close()。临时启动时,按照 R2 第 138-150 行,使用 Kotlinuse { }(scenario 实现了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
}
}
模式:错误与正确——在 lambda 之外保存 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)")
}
模式:错误做法 vs 正确做法——依赖 “Don't keep activities” 进行重建测试
// 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的 lambda 函数体之外保存其返回的activity引用。recreate()和close()会使其失效。 - 不得依赖
Settings.Global.ALWAYS_FINISH_ACTIVITIES强制重建——ActivityScenario不允许这样做。请使用scenario.recreate()。 - 必须在
src/androidTest/AndroidManifest.xml或src/debug/AndroidManifest.xml中声明自定义测试 Activity。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(或更高),以便 Hilt 在 Activity 启动前完成安装。
验证
-
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_*进行断言的测试使用launchActivityForResult(而非普通的launch)。
参考资料
- Android Developers — 测试应用的 Activity(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 行 — 两种ActivityScenarioRule混淆的情况及标准 FQN。