Activity Testing 翻译

Activity 测试比较特殊,它依赖于 Android instrumentation 框架。与其它组件不同 Activity 基于一些回调函数有一个复杂的生命周期;除非用 instrumentation 我们不能直接调用这些回调函数。另外,从程序向用户接口发送事件的唯一方法也是用 instrumentation。

该文档描述了和用 instrumentation 和其它测试工具来测试 Activity。这里假设你已经阅读了 Testing Fundamentals 中介绍 Andoid 测试和 instrumentation 框架的部分。

The Activity Testing API


Activity 测试的基类就是 InstrumentationTestCase,它为用于测试 Activity 的 test case 子类提供 instrumentation。

 

这个基类为针对 Activity 的测试提供以下功能:

  • 控制生命周期:有了 instrumentation,我们可以用 test case 类提供的函数控制待测 Activity 的启动、暂停、销毁。
  • 依赖注入:通过 instrumentation,我们可以创建诸如 Context、Application 这些系统对象的模拟对象,并用这些模拟对象运行待测 Activity。这有助于我们控制测试环境,并和产品环境分离。我们还可以自定义 Intent 并用它启动一个 Activity。
  • 和用户接口交互:通过 instrumentation 我们可以直接向待测 Activity 的 UI 发送按键或者触摸事件。

通过继承 TestCase 和 Asser,这些 Activity 测试类同样有 JUnit 框架的功能。

主要的两个测试子类是 ActivityInstrumentationTestCase2 和 ActivityUnitTestCase。针对那些通过非标准模式加载的 Activity ,我们要用 SingleLaunchActivityTestCase 来测试。

ActivityInstrumentationTestCase2

ActivityInstrumentationTestCase2 这个 test case 类用一个通用的底层系统来对一个应用程序中的一个或多个 Activity 进行功能测试。它用一个标准的系统 Context(非模拟的),在待测应用的一个通用实例(非模拟的)中运行 Activity。它允许你向待测 Activity 发送模拟 Intent,因此用过它,你可以对一个能响应多种 intent 的 Activity 进行测试,或这对一个想从 Intent 中取得某种数据的Activity 进行测试,又或者两种功能都有的 Activity 也可以。但是要注意,由于它不允许模拟 Context ,Application 对象,所以测试不能独立于生产系统。

ActivityUnitTestCase

ActivityUnitTestCase 这个 test case 类用来对一个 Activity 进行独立测试。在启动 Activity 之前,可以注入一个模拟 Context 或者 Application。通过它我们可以在一个独立的环境中运行一个 Activity 测试,可以对一些和 Android 系统没有交互的函数进行单元测试。尽管通过调用 Activity.startActivity(Intent) 函数我们可以查看所接收的参数,但是我们不能发送一个模拟 Intent 给待测 Activity。

SingleLanunchActivityTestCase

SingleLanunchActivityTestCase 可以很方便地对单个 Activity 进行测试,并且所在的测试环境不会在过程中被重置。它对 setUp() 和 tearDown() 的调用只有一次,而不是对每个测试函数都调用一次。它不允许注入任何模拟对象。

这个 test case 更有利于测试那些运行在非 standard 模式的 Activity。它可以确保测试的 fixture 不会在测试的工程中被重置。所以它可以对 Activity 中多个相关联的功能进行测试。

Mock objects and activity testing

这一部分介绍在 Activity 测试中模拟对象的用法,它们定义在 android.test.mock 包中。

模拟对象 MockApplication 仅用在使用 ActivityUnitTestCase 测试 Activity 的情况。默认情况下,ActivityUnitTestCase 会创建一个隐藏的 MockApplication 用来代替待测应用的 Application 对象。我们可以通过 setApplication() 函数注入自己的对象。

Assertions for activity testing

ViewAsserts 类中定义了一些针对 View 的断言。可以用它来验证 View 对象的对齐方式和位置,并且可以查看 ViewGroup 对象的状态。

What To Test


  • 输入验证:一个 Activity 能否对 EditText View 中输入的值做出正确的响应。做一个按键事件序列,发给 Activity,然后用 findViewById(int) 查看 View 的状态。你可以验证一个正确的按键事件序列能否激活 OK 按钮,而一个错误的序列能否把按钮变为 disabled 状态。你也可以在 View 设置一些错误信息来测试 Acivity 的响应。
  • 生命周期事件:测试应用程序中的每个 Activity 是否能正确处理生命周期事件。一般情况下,生命周期事件是由用户或者系统的一些动作引起,这些事件会激发一个诸如 onCreate() 或者 onClick() 函数。例如,一个 Activity 应当在响应暂停或者销毁时保存它的状态。要记得即使是屏幕方向的一个改变也会导致当前 Activity 被销毁,所以你应针对一些特殊的设备动作进行测试,以确认应用程序的状态不会被意外丢失。
  • Intents:测试每一个 Activity 能否正确处理 manifest 文件中 intent 过滤器所列的 Intent。你可以用 ActivityInstrumentationTestCase2 向待测 Activity 发送模拟 Intent。
  • 运行时配置变更:当程序正在运行的时候改变设备的配置,来测试每一个 Activity 能否正确响应。这些配置包括设备的方向,当前语言等等。如何处理这些变化,详情请参考 Handling Runtime Changes
  • 屏幕大小好分辨率:在发布你的应用程序之前,请确保测试你想运行的所有屏幕大小和密度的设备。你可以用多个 AVD 来测试多个尺寸和密度的屏幕,也可以直接在所需设备上测试。更多详情请参考 Supporting Multiple Screens

Next Steps


要了解如何在 Eclipse 中设置和运行测试,请参考 Testing from Eclipse with ADT。如果你用的不是 Eclipse 请参考 Testing from Other IDEs

如果你要一步一步地学习测试 Activity 。请参考 Activity Testing Tutorial,它指导你针对一个 Activity-oriented 应用创建一个完整的测试方案。

Appendix: UI Testing Notes


接下来这几节讲解测试 Android 应用 UI 的一些要点,特别对在测试过程中如何处理来自于 UI 线程的动作,触摸、按键事件,解锁 Home Screen 有很大帮助。

Testing on the UI thread

应用程序的 Activity 运行在应用程序的 UI 线程中。一旦 UI 被初始化,比如在 Activity 的 onCreate() 函数中,之后所有和 UI 的交互都必须在 UI 线程中进行。以正常的方式运行程序,它有权限访问该线程也不必做什么特别的操作。

当运行一个应用程序的测试的时候就有所不同了。通过基于 instrumentation 的类,我们可以调用待测应用 UI 相关的函数。而其它的测试类就不允许这么干。可以用 @UIThreadTest 给函数注释让整个函数在 UI 线程中运行。注意这样做将使函数的所有语句都运行在 UI 线程中。那些和 UI 没有交互的函数不允许这样做;例如,不能(在 UI 线程中)调用 Instrumentation.waitForIdleSync() 函数。

如果想让测试函数的一部分语句子在 UI 线程中运行,就要创建一个 Runnable 匿名类,然后把想(在 UI 线程中)运行的语句放在 run() 函数中,并把该类的实例作为参数传给 appActivity.runOnUiThread() 函数,这个appActivity 就是待测 Activity 的实例。

例如下面的代码:实例化一个 Activity 用来测试,为该 Activity 中显示的一个 Spinner 请求焦点,然后发送一个按键事件给它。要注意的是:不允许waiForIdleSync 函数和 sendKeys 函数运行在 UI 线程中。

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 references 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 aTest() {
      /*       * 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);

Turning off touch mode

如果要用测试代码向设备或者模拟器发送按键事件,从而控制它们,那么必须关掉它们的触摸模式,否则这些按键事件就会被忽略掉。

在调用 getActivity() 启动 Activity 之前调用 ActivityInstrumentationTestCase2.setActivityTouchMode(false) 就可以关掉触摸模式。必须在一个非 UI 线程中运行的函数中调用该函数。因此,你不能在一个有 @UIThread 注释的函数中调用这个触摸模式函数,而应该在 setUP() 函数中调用。

Unlocking the emulator or device

你会发现,如果模拟器或者设备的 home sceen 处于键盘锁锁定状态 UI 测试就无法正常工作。这是因为待测应用无法收到从 sendKeys() 发送的按键事件。为了避免这个问题,最好的方法是在模拟器或设备启动后就关闭 home screen 的键盘锁。

也可以显式地关闭这个键盘锁。要这样做需要在 manifest 文件中加一个权限,然后在待测应用程序中关闭键盘锁。但是要注意,在发布这个应用程序之前也必须把这些代码移出,或者关闭这些代码。

加这个权限,就是在 <manifest> 节点加一个子节点 <uses-permission android:name=”android.permission.DISABLE_KEYGUARD”>。然后在待测 Activity 的 onCreate()函数中加入下面代码来关闭键盘锁:

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

其中的 activity_classname 就是待测 Activity 的类名。

Trobleshooting UI tests

这一节列出了我们在 UI 测试中会经常遇到的失败的(failures) 测试以及原因:

WrongThreadException

Problem:

对于这个失败的测试,失败栈信息中有下面的错误信息:

android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

Probable Cause:

如果在一个 UI 线程之外向该 UI 线程发送 UI 事件就会经常发生这种错误。这通常是因为你在测试代码中发送 UI 事件,而又没有用 @UIThread 注释也没有用 runOnUiThead() 函数。就是说测试函数企图在 UI 线程之外和 UI 做交互。

Sugested Resolution:

在 UI 线程总进行 UI 交互。用有 instrumentation 的 test case 类。详细信息请参考前面的 Testing on the UI Thread 章节。

java.lang.RuntimeException

Problem:

对于这个失败的测试,失败栈信息中有下面的错误信息:

java.lang.RuntimeException: This method can not be called from the main application thread

Probable Cause:

如果你的测试函数已经有了 @UiThreadTest 注释,但是又企图做一些 UI 线程以外的事情,或者又去调用 runOnUiThread() 函数。

Suggested Resolution:

移除 @UiThreadTest 注释,不要调用 runOnUiThead() 函数,或者重构你的测试。

 

posted @ 2013-10-30 14:30  zhaoke5421  阅读(332)  评论(0编辑  收藏  举报