mcpField
L
launching-activities-with-activityscenario

launching-activities-with-activityscenario

github.com/skydoves测试与质量33962 次安装22 周前更新
代码仓库

关于 launching-activities-with-activityscenario

面向 Android 仪器测试的技能指南,说明如何用 ActivityScenario 启动 Activity 并控制其生命周期。内容涵盖 UI 线程访问、配置变更重建、结果读取和 ActivityScenarioRule 的使用。

主要功能

  1. 01启动并控制 Activity 生命周期
  2. 02通过 recreate 模拟配置变更
  3. 03在 UI 线程读取或修改 Activity
  4. 04读取 Activity.RESULT_* 返回值
  5. 05使用 ActivityScenarioRule 自动管理场景

适用场景

  1. 01替换已弃用的 ActivityTestRule
  2. 02测试旋转等配置变更后的状态
  3. 03验证 Activity 返回结果
  4. 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,优先使用 ActivityScenarioRule JUnit4 封装。 唯一规范的 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 行,使用 Kotlin use { }(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。

评论

支持 Markdown