☰
Android Activity 测试实战指南:基于 Instrumentation 的生命周期、Intent 与 UI 交互自动化验证
2026/10/10 5:41:42 网站建设 项目流程
  • 文档
  • 教程
  • 知识库

【免费下载链接】android-tech-frontier

【停止维护】一个定期翻译国外Android优质的技术、开源库、软件架构设计、测试等文章的开源项目

项目地址:https://gitcode.com/gh_mirrors/an/android-tech-frontier
点击查看免费下载

Activity 是 Android 应用中最复杂的组件之一,它的生命周期方法无法被直接调用,因此测试手段与其他组件截然不同。本篇指南以 android-tech-frontier 仓库收录的官方 Activity Testing 文档为核心,系统讲解基于 Android Instrumentation 测试框架的 Activity 测试 API、三种核心测试子类、伪 Object 注入、ViewAsserts 断言,以及主线程 UI 测试的注意事项与常见错误排查。读完本文,你将掌握如何在项目中编写可运行的 Activity 测试用例,覆盖输入合法性、生命周期轮转、Intent 处理和运行时配置变更等场景,并理解 Instrumentation 测试与现代 UI 测试框架(Espresso、UiAutomator)之间的承接关系。


一、为什么 Activity 测试与其他组件测试不同

Activity 测试依赖 Android Instrumentation 测试框架。与普通 Java 类或 Service 不同,Activity 拥有复杂的生命周期,其生命周期函数(onCreate、onResume、onPause、onDestroy等)不能直接被测试代码调用,只能通过 Instrumentation 发送事件来间接触发。

Instrumentation 是一个由系统注入的、与测试代码运行在同一进程的钩子,它由测试包在 Activity 启动前通过InstrumentationTestRunner创建。正是通过这层代理,测试代码才能"遥控"被测 Activity 的行为——这也正是 Activity 测试区别于普通 JUnit 单元测试的根本所在。

在阅读本篇之前,建议先了解 Android 自动化测试与 Instrumentation 框架的基础知识,仓库中相关的背景资料还包括:

  • 使用Android-Studio进行单元测试:介绍测试类的基本形态与运行配置;
  • Android 进行单元测试难在哪-序 及其 part1:分析 Activity 难以直接做纯单元测试的根本原因。

二、Activity 测试 API 介绍

Activity 测试的基类是InstrumentationTestCase,它为 Activity 测试所用的几个类提供 Instrumentation 支持。在 Activity 测试中,这个基类提供以下三个核心功能:

能力说明
生命周期控制使用 Instrumentation 调用 Activity 的onResume、onPause、onDestroy等方法,从而控制其生命周期
依赖注入使用 Instrumentation 制造伪 Context 或伪 Application,更好地控制测试环境,并通过自定义 Intent 启动 Activity
用户界面交互使用 Instrumentation 直接发送按键信息和触屏事件

此外,Activity 测试类通过继承TestCase与Assert实现了 JUnit 测试框架,测试过程中可以直接使用assertEquals、assertTrue等 JUnit 断言方法。

在实际编写测试时,主要使用的两个测试子类是ActivityInstrumentationTestCase2和ActivityUnitTestCase;而当 Activity 的launchMode设置为非standard属性时,则需要使用SingleLaunchActivityTestCase。

注意:测试方法必须以test开头(如testAdd),否则测试运行器无法识别该方法,这与 使用Android-Studio进行单元测试 中强调的规则一致。

2.1 ActivityInstrumentationTestCase2

这个类用来测试同一个应用的多个 Activity,它拥有正常的应用实例环境和 Context,可以接触到正常的系统结构(文件、数据库等)。你可以在测试中发送伪装的 Intent 给 Activity,验证程序是否对各种 Intent 进行了正确处理。

关键限制:这个类中不能使用伪 Context 和伪 Application,因此无法将测试从系统环境中独立出来。

它适合验证 Activity 与系统真实交互的行为,例如访问 ContentProvider、读写文件或数据库后 UI 是否正确更新。

2.2 ActivityUnitTestCase

这个类用来测试单个 Activity,可以运行在独立的测试环境中。你可以在启动 Activity 前设置 Context 或 Application(两者同时设置也可以)来构造模拟环境,从而在虚拟环境中测试程序,而不会对系统、文件等造成实际影响。

关键限制:在该测试类下无法向正在测试的 Activity 发送 Intent,不过可以在启动 Activity 时调用Activity.startActivity(Intent)来查看接收到的参数。

它适合对 Activity 的独立逻辑(如解析 Intent 参数、初始化视图、处理输入数据)做隔离验证,特别适合验证 Activity 对异常数据或边界条件的反应。

2.3 SingleLaunchActivityTestCase

这个类用来方便地测试单个 Activity。由于它只会调用一次setUp()和tearDown()(而不是每个测试方法调用一次),因此该实例中的所有测试方法共享同一个测试环境。在此类中不允许加入任何伪 Object。

这个类在测试 Activity 的launchMode属性不是standard时非常有用——例如singleTop、singleTask模式下的 Activity 可能被多次复用而不是重新创建。它保证了测试环境的不变,因此可以用来测试 Activity 是否对多次重复调用做出了正确应对。

2.4 三个测试类的选择对比

测试类适用场景是否可使用真实系统环境是否可使用伪 Context/Application是否可发送 Intent是否共享测试环境
ActivityInstrumentationTestCase2同一应用多个 Activity是否是否(每个方法独立)
ActivityUnitTestCase单个 Activity 隔离测试否(独立虚拟环境)是否(仅可检查startActivity参数)否(每个方法独立)
SingleLaunchActivityTestCase单个 Activity、非standardlaunchMode是否视具体子类而定是(仅调用一次 setUp/tearDown)

三、Activity 中的伪 Object 使用

android.test.mock包定义了一系列伪 Object,用于隔离被测 Activity 与真实系统的耦合。文档中重点介绍的是MockApplication:

  • MockApplication只可以在ActivityUnitTestCase中使用,通过setApplication()指定,必须在startActivity调用之前设置;
  • 若不指定,ActivityUnitTestCase会自动生成一个伪 Application;
  • 伪 Context 则可以通过setActivityContext()指定。

结合 Google+ 团队的 Android UI 测试 中的经验,这种"封闭式测试"策略的价值在于:通过注入伪依赖(伪服务器、伪 Context、伪 Application)将测试与外部环境隔离,从而显著提高测试速度与稳定性。这与 Activity 测试中的伪 Object 注入思路一脉相承——把不可控的系统资源替换为可控的测试替身。

四、Activity 测试中的判断语句:ViewAsserts

ViewAsserts定义了供 View 使用的一些判断语句,可以检查 View 内容的位置、对齐情况和状态。配合findViewById(int)获取 View 引用后,即可在测试中断言:

  • View 是否可见、是否被正确布局;
  • View 的坐标位置、与其他 View 的对齐关系;
  • 按钮等控件的 enabled 状态。

示例用法:

// 断言按钮处于可用状态 assertEquals(true, button.isEnabled()); // 使用 ViewAsserts 检查布局位置 ViewAsserts.assertHorizontalAlignment(anchorView, targetView);

五、我们要测试什么:五类核心测试场景

5.1 输入合法性

测试 Activity 是否对EditText的各类输入做出正常反应。思路是:模拟输入一串信息发送给 Activity,然后使用findViewById(int)检查 View 的状态。

典型做法:设置一个Button,当输入异常时将其 disable,输入正常时 enable,然后通过断言验证:

assertEquals(button.isEnabled(), true);

还可以通过设置错误输入,检查 Activity 是否进行了合适的处理(例如弹出错误提示、阻止提交等)。

5.2 生命周期控制

测试 Activity 是否正常处理了生命周期的各个轮转情况。一个合格的 Activity 应该在 pause 或 destroy 时保存一些下次运行时需要的状态。

需要特别记住:屏幕布局方向改变(如竖屏变为横屏)会引起 Activity 的 destroy 过程,因此你应该保证设备外部变化(如旋转、切换应用)不会引起应用数据丢失。这正是 Android 进行单元测试难在哪-part1 中提到的"预测试状态 / 测试后状态"难以直接访问的典型场景,也是 Activity 需要专门测试框架而非纯 JUnit 的原因之一。

5.3 Intent 测试

测试每个 Activity 是否正常处理了它在 manifest 文件中<intent-filter>属性声明的 Intent。可以在ActivityInstrumentationTestCase2测试中构造并发送伪 Intent 给 Activity,然后验证其对 Intent 携带参数的解析与响应是否正确。

5.4 运行时设置改动

模拟设备的设置改变(如屏幕方向改变、语言改变等),测试正在运行中的 Activity 的应对是否正确。这类测试可以捕捉到因配置变更导致的状态丢失问题——例如旋转屏幕后用户输入的内容是否保留、当前选中的 Tab 是否复位等。

5.5 设备尺寸以及分辨率的适配

在应用发布前,应保证它在各式各样的屏幕上运行正常。可以通过ViewAsserts自动化检测布局情况,并在各式各样的 Android 虚拟机上运行测试,或直接在目标机型上运行,从而验证不同分辨率下的布局适配。

六、UI 测试中需要注意的地方:主线程、按键与锁屏

以下内容是 UI 测试的实践要点,特别是涉及主线程中动作(触屏、输入和锁屏等事件)的测试时,应仔细阅读。

6.1 在主线程上做测试

应用的 Activities 运行在主线程上。一旦 UI 界面实例化(例如当 Activity 的onCreate方法被调用后 UI 即会实例化),所有和 UI 界面交互的事件都必须在主线程中发生。正常启动应用时必须遵循这条规则,程序才能正常运行。但是在以 Instrumentation 为基础的测试中,你可以在测试程序中直接对 UI 进行操作。

有两种在主线程中运行测试代码的方式:

方式一:让整个测试函数都在主线程中运行——添加@UiThreadTest注解。

@UiThreadTest public void testSomething() { // 整个函数中的所有语句都运行在主线程中 }

但要注意:这种情况下整个函数中所有语句都运行在主线程中,不和 UI 组件交互的语句是不允许的,例如Instrumentation.waitForIdleSync()。

方式二:让测试函数中的一部分在主线程中运行——调用runOnUiThread()。

将想要运行在主线程中的代码放入一个匿名内部类Runnable中,传给runOnUiThread()方法(appActivity是你在测试中拥有的 Activity 实例)。

下面这段代码演示了完整的示例:测试一个 Activity 实例,控制Spinner获取焦点,并发送给它一个触屏事件。注意waitForIdleSync和sendKeys不允许运行在主线程中:

private MyActivity mActivity; // MyActivity is the class name of the app under test private Spinner mSpinner; protected void setUp() throws Exception { super.setUp(); mInstrumentation = getInstrumentation(); mActivity = getActivity(); // get a reference to the app under test /* * Get a reference to the main widget of the app under test, a Spinner */ mSpinner = (Spinner) mActivity.findViewById( com.android.demo.myactivity.R.id.Spinner01); } public void testSpinnerInteraction() { /* * Request focus for the Spinner, so that the test can send key events to it. * This request must be run on the UI thread. To do this, use the * runOnUiThread method and pass it a Runnable that contains a call to * requestFocus on the Spinner. */ mActivity.runOnUiThread(new Runnable() { public void run() { mSpinner.requestFocus(); } }); mInstrumentation.waitForIdleSync(); this.sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); }

代码逻辑拆解:

  1. setUp()中通过getActivity()获取被测 Activity 的引用,并用findViewById拿到主控件Spinner;
  2. 测试方法中先用runOnUiThread让Spinner在主线程中请求焦点;
  3. 调用waitForIdleSync()等待 UI 线程空闲,确保焦点请求完成;
  4. 最后通过sendKeys(KeyEvent.KEYCODE_DPAD_CENTER)向控件发送确认键事件,模拟用户操作。

6.2 屏蔽掉物理按键和触屏

为了让模拟器或设备接收到测试程序发来的按键事件,需要屏蔽设备的物理按键和触屏事件。如果不这样做,测试程序发送的事件不会起作用。

可以通过调用ActivityInstrumentationTestCase2.setActivityTouchMode(false)实现,且必须在getActivity()调用之前执行。

注意:不能在运行于主线程的语句中调用它,因此它不可以出现在含有@UiThreadTest注解的函数中。最佳做法是在setUp()方法中完成这一设置:

protected void setUp() throws Exception { super.setUp(); setActivityTouchMode(false); // 必须在 getActivity() 之前 mActivity = getActivity(); }

6.3 先将设备解锁再进行测试

UI 测试在屏幕锁屏(或存在其他加密手段)时无法使用,因为这种情况下应用无法接收到sendKeys()发送的事件。最好的解决办法是先解锁设备再进行测试。

当然也可以通过代码显式地禁用锁屏——在onCreate中加入以下代码,不过需要为应用添加权限<uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>:

mKeyGuardManager = (KeyguardManager) getSystemService(KEYGUARD_SERVICE); mLock = mKeyGuardManager.newKeyguardLock("activity_classname"); mLock.disableKeyguard();

6.4 疑难解答

以下列出 UI 测试中最容易遇到的两类崩溃错误及其解决办法。

WrongThreadException

  • 问题:测试崩溃,错误消息为android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
  • 可能的原因:从非 UI 线程中和 UI 进行交互。当在测试中与 UI 控件交互而不加@UiThreadTest注解、或不放进runOnUiThread()方法时,命令会在非主线程中执行,从而触发该错误。
  • 建议:在主线程中和 UI 交互,并使用支持 Instrumentation 的测试类。

java.lang.RuntimeException

  • 问题:测试崩溃,错误消息为java.lang.RuntimeException: This method can not be called from the main application thread
  • 可能的原因:在带有@UiThreadTest注解的函数中调用了runOnUiThread()或其他不能在 UI 线程中运行的语句。
  • 建议:尝试去掉@UiThreadTest注解,或去掉runOnUiThread(),或重写测试程序来解决。

七、从 Instrumentation 到现代 UI 测试框架的演进

本文介绍的InstrumentationTestCase体系是 Android 早期官方测试方案的核心,其"通过 Instrumentation 驱动生命周期、发送按键与触屏事件、在主线程操作 UI"的思想被后来的框架完整继承并封装:

  • Espresso:在 Instrumentation 基础上封装了更简洁的 API,用onView(...).perform(...).check(...)取代了繁琐的runOnUiThread+sendKeys组合,并对同步等待做了自动化处理。参见 Android-Espresso测试框架介绍;
  • UiAutomator:面向黑盒 UI 测试,通过UiDevice、By、Until等 API 跨应用模拟用户行为,同样运行在 Instrumentation 之上,参见 Android UI 自动化测试 与 Android-UI自动化测试。

理解本文中的底层机制(主线程约束、锁屏影响、WrongThreadException成因),能帮助你在使用更高级的框架时准确判断错误的根源。同时,Google+ 团队的 Android UI 测试 还从工程实践角度给出了补充建议:UI 测试应优先使用封闭式测试(注入伪服务器与伪依赖)而非端到端测试,以保证测试速度与稳定性。

八、总结

Activity 测试的核心在于理解其特殊性:生命周期不能直接调用、UI 操作必须在主线程、物理按键需要先被屏蔽、设备锁屏会阻断事件注入。在此基础上,按照"选择正确的测试子类 → 构造测试环境(伪 Object 或真实环境)→ 通过 Instrumentation 驱动生命周期与 UI 事件 → 用 JUnit 断言与 ViewAsserts 验证结果"的路径,即可为 Activity 构建覆盖输入合法性、生命周期轮转、Intent 处理、运行时配置变更与多尺寸屏幕适配的自动化测试体系。

本仓库中 issue-22/Android-Activity测试.md 保留了该主题的完整译文,其余测试相关文章(如 使用Android-Studio进行单元测试、Android-Espresso测试框架介绍、Android UI 自动化测试)可作为进阶参考,帮助你构建从单元测试、Instrumentation 测试到现代 UI 测试框架的完整测试栈。

  • 文档
  • 教程
  • 知识库

【免费下载链接】android-tech-frontier

【停止维护】一个定期翻译国外Android优质的技术、开源库、软件架构设计、测试等文章的开源项目

项目地址:https://gitcode.com/gh_mirrors/an/android-tech-frontier
点击查看免费下载

相关推荐

上一篇:如何快速掌握中文书法风格迁移:从零到精通的完整实践指南
下一篇:MelonLoader技术全解析:Unity游戏模组加载器的深度探索

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询