4. 单一视图夹具:使用 ActivityScenario.launch 启动 Activity、获取 View 引用
4.1 原理
单一视图夹具(Single View Fixture)是 Espresso 测试中最基础的测试模式。它的核心思想是:通过 ActivityScenario 直接启动一个特定的 Activity,在可控的生命周期内获取该 Activity 视图树中某个 View 的引用,进而对它执行操作或断言。在 Espresso 与 Compose 混用场景下,单一视图夹具为原生 View 体系提供了隔离的测试环境,避免了对 Compose 节点的干扰,是混合测试中“原生侧”的锚点。
夹具(Fixture)的本质是建立一个固定、可重复的测试上下文。单一视图夹具将 Activity 作为这个上下文的载体,ActivityScenario 则负责管理 Activity 的启动、状态迁移和销毁,确保每个测试用例都在干净的 Activity 实例上运行。
4.2 实现流程
单一视图夹具的测试用例遵循六个步骤,每个步骤对应流程图中一个节点:
- 定义测试方法:使用
@Test注解标记测试函数,声明可抛出的异常。 - 启动 Activity:调用
ActivityScenario.launch(MainActivity::class.java)创建并启动目标 Activity。 - 等待生命周期就绪:
ActivityScenario自动将 Activity 推进到onCreate状态,此时视图树已初始化。 - 获取 Activity 引用:通过
scenario.onActivity { activity -> ... }在 UI 线程安全地获得 Activity 实例。 - 定位目标 View:在回调内部调用
activity.findViewById(R.id.view_id)获取具体 View 对象,或使用 Espresso 的onView()匹配器进行定位。 - 执行操作与断言:对 View 执行点击、输入等操作,并使用
check()或 JUnit 断言验证结果。
4.3 API 参考
| 方法 / 接口 | 参数 | 说明 |
|---|---|---|
ActivityScenario.launch(Class<A>) | Activity 的 Class 对象 | 启动目标 Activity,返回 ActivityScenario 实例 |
scenario.onActivity(ActivityAction<A>) | Lambda: (Activity) -> Unit | 在 UI 线程执行操作,可获取 Activity 引用 |
scenario.moveToState(Lifecycle.State) | CREATED / STARTED / RESUMED | 手动迁移 Activity 生命周期状态 |
scenario.recreate() | 无 | 模拟 Activity 重建(如屏幕旋转) |
scenario.close() | 无 | 销毁 Activity 并释放资源 |
onView(ViewMatcher) | withId() / withText() 等 | Espresso 核心 API,在视图树中搜索匹配的 View |
4.4 代码示例
以下是一个完整的单一视图夹具测试用例,演示从 Activity 启动到 View 操作与断言的完整链路:
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class MainActivityFixtureTest {
@Test
fun submitButton_click_updatesResultText() {
// 1. 启动 Activity
val scenario = ActivityScenario.launch(MainActivity::class.java)
// 2. 通过 onActivity 获取 View 引用并执行操作
scenario.onActivity { activity ->
val button = activity.findViewById<Button>(R.id.button_submit)
button.performClick()
}
// 3. 使用 Espresso 验证结果 View 的文本
onView(withId(R.id.text_result))
.check(matches(withText("已提交")))
// 4. 清理资源
scenario.close()
}
}
以上代码中,
onActivity 回调运行在 UI 线程,因此可以直接调用 performClick()。如果需要操作完成后立即进行 Espresso 断言,确保所有 UI 更新已在主线程完成——onView() 会自动等待 UI 空闲。4.5 调试与常见错误
- Activity 未在 AndroidManifest 中注册:启动时抛出
ActivityNotFoundException。检查AndroidManifest.xml中是否包含目标 Activity 的声明。 - 在 onActivity 外部持有 Activity 引用:
onActivity是闭包作用域,外部持有 Activity 引用会导致内存泄漏或失效引用。始终在回调内部完成所有 View 操作。 - findViewById 返回 null:检查布局文件中是否存在对应 ID,以及 Activity 是否正确调用了
setContentView()。如果视图在onCreate之后动态添加,需要等待添加完成。 - Espresso 断言时 View 未显示:确保 Activity 处于
RESUMED状态,且目标 View 的可见性为VISIBLE。使用scenario.moveToState(Lifecycle.State.RESUMED)可强制迁移状态。 - “No activities found” 错误:测试模块的
build.gradle中缺少androidx.test:core-ktx依赖。添加后同步项目。
⚠️ 常见陷阱:不要在
@Before 方法中启动 Activity,然后在 @Test 中复用 ActivityScenario 实例。每个测试用例都应该调用 launch() 获取全新的 Activity 实例,避免状态污染。如果必须在多个测试间共享,使用 ActivityScenarioRule 并确保每个测试方法独立启动 Activity。
4.6 性能与稳定性优化
- 复用 ActivityScenarioRule:使用
@get:Rule val rule = ActivityScenarioRule(MainActivity::class.java)让框架自动管理 Activity 的启动与销毁,减少样板代码,同时避免忘记调用close()导致的资源泄漏。 - 避免在 onActivity 中执行耗时操作:
onActivity运行在 UI 线程,任何阻塞操作都会导致 ANR。将网络请求、文件读写等耗时任务放到测试线程,或使用IdlingResource等待异步完成。 - 最小化 View 查找范围:如果在复杂的视图层次中定位 View,优先使用
onView(withId(...))而非遍历findViewById链。Espresso 的匹配器经过优化,查找效率高于手动遍历。 - 合理使用 IdlingResource:如果 Activity 启动时执行了异步动画或数据加载,注册自定义
IdlingResource告知 Espresso 等待这些操作完成,避免因视图未就绪导致断言失败。 - 控制生命周期状态:使用
moveToState()精确控制 Activity 生命周期,在需要测试特定状态(如onPause后的行为)时非常有效,但不要滥用——默认RESUMED状态已覆盖 90% 的测试场景。
💡 稳定性建议:在团队实践中,建议将单一视图夹具的
launch + onActivity + close 封装为扩展函数或基类方法,统一管理 Activity 的生命周期。这不仅减少重复代码,还能确保所有测试用例遵循相同的资源清理策略,从制度上避免泄漏。