4. 单一视图夹具:使用 ActivityScenario.launch 启动 Activity、获取 View 引用

4.1 原理

单一视图夹具(Single View Fixture)是 Espresso 测试中最基础的测试模式。它的核心思想是:通过 ActivityScenario 直接启动一个特定的 Activity,在可控的生命周期内获取该 Activity 视图树中某个 View 的引用,进而对它执行操作或断言。在 Espresso 与 Compose 混用场景下,单一视图夹具为原生 View 体系提供了隔离的测试环境,避免了对 Compose 节点的干扰,是混合测试中“原生侧”的锚点。

夹具(Fixture)的本质是建立一个固定、可重复的测试上下文。单一视图夹具将 Activity 作为这个上下文的载体,ActivityScenario 则负责管理 Activity 的启动、状态迁移和销毁,确保每个测试用例都在干净的 Activity 实例上运行。

单一视图夹具工作流程 单一视图夹具工作流程 @Test 测试用例 ActivityScenario.launch() Activity.onCreate() 生命周期 onActivity { } 回调 findViewById() 获取 View Espresso 交互与断言 单一视图夹具 = ActivityScenario + View 引用 适用于单 Activity 测试与原生 View 交互场景

4.2 实现流程

单一视图夹具的测试用例遵循六个步骤,每个步骤对应流程图中一个节点:

  1. 定义测试方法:使用 @Test 注解标记测试函数,声明可抛出的异常。
  2. 启动 Activity:调用 ActivityScenario.launch(MainActivity::class.java) 创建并启动目标 Activity。
  3. 等待生命周期就绪:ActivityScenario 自动将 Activity 推进到 onCreate 状态,此时视图树已初始化。
  4. 获取 Activity 引用:通过 scenario.onActivity { activity -> ... } 在 UI 线程安全地获得 Activity 实例。
  5. 定位目标 View:在回调内部调用 activity.findViewById(R.id.view_id) 获取具体 View 对象,或使用 Espresso 的 onView() 匹配器进行定位。
  6. 执行操作与断言:对 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 的生命周期。这不仅减少重复代码,还能确保所有测试用例遵循相同的资源清理策略,从制度上避免泄漏。