Android 测试框架是其开发环境的一部分,它提供了一个测试架构和一个强大的工具集来帮助我们对程序的方方面面进行测试,包括从单元测试到框架测试各个层次。
该测试框架有三个关键点:
- Android 测试套件是基于 JUnit 的。我们可以单纯的使用 JUnit 来测试一个没有调用 Android API 的类,或者用 Android 提供的扩展 JUnit 测试 Android 组件。如果在 Android 测试方面你还是一个小白,你可以从用 AndroidTestCase 这种通用类开始,然后再去使用更复杂的类。
- Android 扩展 JUnit 提供了特定组件测试用例类。这些类提供辅助函数,通过这些辅助函数我们可以创建模拟(mock)对象和模拟函数来帮助我们控制一个特定组件的生命周期。
- 测试套件拥有和待测程序一样的包结构,所以你不需要为了设计、构建测试而学习新的工具和技术。
- Eclipse ADT 提供了用于构建和测试的工具,其它 IDE 中可以以命令行的方式使用这些工具。这些工具从待测工程中获取信息,并用这些信息自动创建 build 文件、manifest 文件和测试程序的目录结构。
- SDK 也提供了 monkeyrunner,这是一套用 Python 测试设备的API。还有UI/Application Execiser Monkey,这是一个命令行工具,通过它我们可以向设备发送伪随机事件来进行压力测试。
这个文档描述了 Android 测试框架的基本原来,包括测试结构、用于测试的 API、用于运行测试和查看测试结果的工具。这里假定你已经掌握了 Android 开发和 JUnit测试的基本知识。
下面是测试框架图:
Test Structure
Android 的 build 和测试工具假定测试 projects 被组织成一个标准的测试结构、测试用例类、测试包和测试 projects。
Android 测试是基于 JUnit的。通常,一个JUnit test 就是一个函数,这个函数用于测试待测项目的部分功能。把这些测试函数所属的类就叫测试用例(test cases)或者测试套件(test suites)。每个测试都是针对待测项目中一个单独模块的独立测试。每个测试用例类都是相关函数的一个容器,另外它也提供一些辅助函数。
在 JUnit 中,我们可以把一个或者多个测试源代码文件构建成一个 class文件。同样,我们也可以用 SDK 构建工具把一个或者多个测试源代码文件构建成 Android测试包中的一个 class 文件。在 JUnit 中我们用一个 test runner 来执行 test class。同样,在 Android 中我们用测试工具加载测试包和待测程序,然后由测试工具调用 Andoid 特定的 test runner。
Test Projects
测试(代码),就像 Android 应用程序(代码)一样被弄成 project。
一个 test project 就是一个文件夹或者是一个 Eclipse project,我们创建的源代码文件夹、manifest 文件和这个测试的其它文件都包含其中。Android SDK 提供了一些工具,通过这些工具我们可以创建、更新 test project,这些工具既有 Eclipse ADT使用方式,也有命令行的使用用方式。这些工具可以创建 test project的源代码文件夹、资源文件夹和 manifest 文件。通过命令行的方式我们还可以创建所需的 Ant build 文件。
通常,我们都应该使用 Android tools 来创建一个 test project。在众多好处中,这些工具尤其可以:
- 自动设置 test package 用 InstrumentationTestRunner 作为 test case runner。我们又必须使用它来运行 Junit tests。
- 为 test package 创建一个恰当的名字。假设待测 project 的 包名是 com.mydomain.myapp,那么 Android tools 就把 test package 的包名设置为 com.mydomain.myapp.test。这有利于我们识别它们之间的关系,以避免系统内的冲突。
- 自动为 test project 创建合适的 build 文件、manifest 文件、和目录结构。这方便我们构建 test package 而不必手动修改 build 文件,也不用手动建立 test project 和待测 project 之间的联接。
尽管我们可以在文件系统的任何地方创建一个 test project,但是最好的做法是把 test project 添加到待测 project 中,这样它的根目录 tests/ 就和 待测 project 的 src/ 处于同一目录层次。这有利于我们快速查找一个应用的相关测试。例如,如果你的待测 project 的根目录是 MyProject,那么你应当用下面的目录结构:
MyProject/
AndroidManifest.xml
res/
...(待测 project 的资源文件)
src/
...(待测 project 的源代码)
tests/
AndroidManifest.xml
res/
...(test project 的资源文件)
src/
...(test project 的源代码)
The Testing API
Android 测试 API 基于 JUnit API 扩展了一个 instrumetation 框架和一些 Android 特有的测试类。
JUnit
我们可以用 JUnit TestCase 对一个没有调用 Android API 的类进行单元测试。我们可以通过 AndroidTestCase 来进行测试依赖于 Android 的对象,该类是 TestCase 的子类。除了 JUnit 框架已有的功能外,AndroidTestCase 也提供了 Android 特有的 setup、teardown 方法和一些辅助函数。
用 JUnit Assert 函数来显示测试结果。该函数对测试的期望结果和实际结果进行比较,如果比较失败它就会抛出一个异常。Android 也提供了一个断言类,它继承了Assert 现有的断言,并且针对 UI 提供来其它的断言类,详情请参阅 Assertion classes 部分。
更深入的学习 JUnit ,请参阅 junit.org 主页的文档。记住,Android testing API 只支持 JUnit 3 的编码风格,不支持 JUnit 4。另外必须使用 Android 的测试运行器 InstrumentationTestRunner 来运行你的测试用例类。在 Running Tests 部分会解释该测试运行器。
Instrumentation
Android instrumentation 在 Android 系统中是一个控制函数集合或者“hooks” ,这些钩子控制一个 Android 组件,使其脱离正常的生命周期。它们也控制着 Android 如果加载应用程序。
正常情况下,一个 Android 组件运行在受系统控制的生命周期内。例如,一个 Activity 对象, 它的生命周期开始于它被一个 Intent 激活时。它的 onCreate()函数被调用,然后是 onResume()函数。当用户启动其它应用时,onPause()函数被调用。如果调用 Activity 的 finish() 函数,那么 onDestroy() 函数会被调用。Android 框架不会提供接口让我们直接调用这些回调函数,但是我们可以通过 Instrumentation 做到这一点。
同样,系统把一个应用中的所有组件都运行在同一进程中。我们可以让一些组件,比如 content provider,运行在独立的进程中,但是我们不能强制一个应用和另外一个已经运行的应用运行在同一进程中。
但是,通过 Instrumentation 我们可以在测试代码中调用这些回调方法。这样我们就可以一步一步的遍历一个组件的生命周期,就好象你在调试组件一样。下面的代码片段演示了如何用 Instrumentation 来测试一个 Activity 的存储动作和恢复状态动作:
//启动待测应用的 main activity
mActivity = getActivity();
//获取这个 activity 的主要 UI 组件,就是一个 Spinner
mSpinner = (Spinner)myActivity.findViewById(com.android.example.spinner.R.id.Spinner01);
//把一个已知的位置设置给这个 Spinner
myActivity.setSpinnerPosition(TEST_STATE_DESTROY_POSITION);
//停止 Activity -应该在 onDestroy() 函数内保存 Spinner 的状态。
mActivity.finish();
//重启 Activity - 应当在 onResume() 函数中恢复 Spinner 之前的状态
mActivity = getActivity();
//获取 Spinner 当前的位置
int currentPosition = mActivity.getSpinnerPosition();
//断言当前位置和之前的位置一样。
assertEquals(TEST_STATE_DESTROY_POSITION, currentPosition);
这里所用的关键函数是 getActivity(),它是属于 instrumentation API 。待测 Activity 直到调用这个函数才启动。我们可以先设置一些高级测试配置,然后再启动Activity。
同样,instrumentation 可以把测试程序和待测程序加载到一个进程中。因为应用组件和它的测试程序在一个进程中,那么测试程序就可以调用组件的函数,并可以修改、检查组件的属性了。
Test case classes
Android 提供了一些继承 TestCase 和 Assert 的 test case 类,这些类另外还提供了 Android 特有的 setup、teardown 和 辅助方法。
AndroidTestCase
AndroidTestCase 是一个有用的通用 test case 类,尤其是你刚接触 Android 测试时。它继承自 TestCase 和 Assert,它提供 JUnit 标准的 setUP() 和 tearDown() 函数,同样也提供了 JUnit 的 断言函数。另外它还提供了一些测试权限的函数,还有一个通过清理某些类的引用来防止内存泄露的函数。
Component-specific test cases
Android 测试框架的一个关键特性就是它有一些针对特定组件的 test case 类。这些对组件的特殊测试需要一些设置、卸载资源(fixture)的函数和一些控制组件生命周期的函数。这些类也提供了设置模拟对象的函数。在 component-specific testing 章节会讨论这些类:
Android 没有为 BroadcastReceiver 单独提供 test case 类。而是通过向(注册它的)组件发送 Intent 对象来验证 BroadcastReceiver 能否正确响应。
ApplicationTestCase
我们用 ApplicationTestCase test case 类来测试 Application 对象的 setup 和 tearDown。这些对象维护着应用程序的全局信息,应用程序包中的所有组件都可以引用这些信息。该 test case 类用于验证 manifest 文件中的 <application> 标签是否正确设置。但是要注意,你不能用该 test case 测试应用程序包中的组件。
InstrumentationTestCase
如果要在一个 test case 类中用 instrumentation 中的函数,必须用 InstrumentationTestCAse 或其子类。Activity test case 继承自该类,并提供了一些其它的函数以帮助进行 Activity 测试。
Assertion classes
因为 Android 的 test case 类继承自 JUnit,所以我们可以用断言函数来显示测试结果。断言函数对一个测试返回的实际结果和一个期望结果进行比较,如果比较失败就跑出一个 AssertionException 。用断言远比用 log 方便,并且性能更好。
除了 JUnit Assert 类提供的函数之外,测试 API 还提供了 MoreAsserts 和 ViewAsserts 两个类:
- MoreAsserts 包含了功能强大的断言,比如 assertContainsRegex(String,String),该函数可以匹配正则表达式。
- ViewAsserts 包含了用于 View 的断言。例如它包含了 assertHasScreenCoordinates(View, View, int ,int),该函数可以测试一个 View 是否包含一个特定的可显示坐标点。这些断言可以对 UI 的几何形状和对齐方式进行简单的测试。
Mock object classes
为了方便在测试中使用依赖注入,Android 提供了一些类来创建系统模拟对象,比如 Context 对象、ContentProvider 对象、ContentResolver 对象和 Serviec 对象。一些 test case 类还提供了虚拟 Intent 对象。 我们用这些对象既可以将测试和系统其它部分隔离开来,又可以在测试中方便地使用依赖注入。这些类都在 android.test 包和 android.test.mock 包中。
模拟对象通过屏蔽或者覆写常规操作来把测试和正在运行的系统隔离开来。例如,MockContentResolver 用自己独立于系统的本地框架代替标准的 Resolver 框架。MockContentResolver 也屏蔽了 notifyChange(Uri,ContentObserver, boolean) 函数,所以测试环境外的 observer 对象就不会不小心被触发了。
模拟对象类也提供了常规类的子类以方便使用依赖注入,这些模拟类里面的函数没有实际的功能,除非你覆写它。例如,MockResource 类是 Resources 的子类,当调用这个模拟类里的所有函数时都只是抛出一个异常。要想使用它,我们只用覆写要用的函数即可。
下面是 Android 提供的虚拟对象类:
Simple mock object classes
MockApplication,MockContext,MockContentProvider,MockCursor,MockDialogInterface,MockPackageManager,和 MockResources 提供了一个简单而又实用的模拟策略。他们是系统中相应类的屏蔽版本(就是空实现),它们中的所有函数都只是抛出 UnsupportedOperationException 异常而已。使用的时候,我们只需覆写需要提供信息的函数来模拟依赖。
注意: MockContentProvider 和 MockCursor 是 API 8 中新提供的。
Resolver mock objects
MockConentResolver 通过屏蔽系统的常规 resolver 框架为 content provider 提供一个隔离的测试。MockContentResolver 使用自己的内部表,而不是用一个给定的 authority 在系统中找一个对应的 content provider。我们必须用 addProvider(String,ContentProvider) 函数显式地把 provider 加到这个表中。
有了它,我们可以把一个模拟的 content provider 和一个 authority 关联起来。我们可以创建一个真实的 provider 而只用它里面的测试数据。我们甚至可以把一个 authority 相应的 provider 设置为 null。事实上,MockContentResolver 对象把我们的测试和包含真实数据的 provider 隔离开来。我们既可以对 provider 进行操作,又可以避免测试影响到真实的数据。
Context for testing
Android 提供了两个对测试很有用的 Context 类:
- IsolatedContext 是一个隔离的 Context,对这个 Context 上文件、目录、数据库进行的操作只发生在测试范围。虽说功能有限,它也提供的 stub code 也足以响应系统的调用。该类允许我们对应用程序的数据进行测试而又不会影响到设备上的真实数据。
- RenamingDelegatingContext 里面的大部分功能其实都由现存的一个 Context 来处理,但是文件和数据库操作都由一个 IsolatedContext 来处理。隔离的部分单独使用一个测试目录,并创建特殊的文件和目录名。我们可以自己控制命名,或者交给构造函数自动处理。
该类为给数据操作设置一个隔离区域提供了一种快速途径,并保留了 Context 的其它常规功能。
Running Tests
Test case 是通过一个 test runner class 运行的,这个 test runner class 对每个测试进行加载、set up 、运行、tear down。test runner 还必须装载上 instrumentation ,以便启动应用程序的系统工具可以控制 test package 如何加载 test case 和待测应用。我们可以在 test package 的 manifest 文件中设置一个值,来告诉 Android 平台用哪个装备了 instrumentation 的 test runner。
InstrumentationTestRunner 是 Android 主要的 test runner class。它继承自 JUnit test runner 框架,并且已经装备了 instrumentation。它可以运行Android 提供的所有 test case 类,并且支持所有的测试类型。
我们可以在 test package 的 manifest 文件的 <instrumentation> 标签指定 InstrumentationTestRunner 或其子类。它不不同 Android 的其它代码,它属于共享库 android.test.runner。要想把包含进项目,我们必须在 <uses-library> 标签指定它。我不必手动去设置这些标签。Eclipse 的 ADT 和 android 命令行工具都可以自动构建它们,并加入 test package 的 manifest 文件。
注意:如果你不想用 InstrumentationTestRunner ,你必须修改 <instrumentation> 标签来指定你想用的类。
要运行 InstrumentationTestRunner,我们要通过 Android tools 来调用系统的内部类,然后内部类来运行它。当我们在 带 ADT 的 Eclipse 中运行测试时,这些类会自动调用。如果我们通过命令行运行测试,我们通过 Android Debug Bridge(adb) 来调用这些类。
这些系统内部类负责加载并启动 test package,首先杀死所有运行待测应用实例的所有进程,然后加载一个新的待测应用的实例。接着就把控制权交给 InstrumentationTestRunner,由 InstrumentationTestRunner 来运行 test package 中的所有 test case。在带有 ADT 的 Eclipse 中通过设置来决定运行哪些 test cass 或者 方法,或者在命令行中使用标记来实现这一点。
既不是系统内部类启动待测应用也不是 InstrumentationTestRunner 启动。而是 test case 直接启动。它既可以调用待测应用中的函数,又可以调用自己的函数,这些函数(自己的函数)可以触发待测项目的生命周期事件。待测应用完全在 test case 的控制之下,这样 test case 就可以在运行测试之前设置测试环境(the test fixturre)。这在之前的代码片段中已经演示了,该代码片段是测试在一个 Activity 中显示一个 Spinner 控件。
想要了解更多关于运行测试的问题,请参阅 Testing from Eclipse with ADT 或者 Testing from Other IDEs。
Seeing Test Results
Android 测试框架把测试结果返回给了启动该测试的工具。如果你在带有 ADT 的 Eclipse 中运行测试,那么测试机过就会显示在一个新开的 JUnit 视图托盘中。如果是通过命令行运行测试,那么测试结果会在 STDOUT 中显示。不管是哪种环境,你都可以看到一个测试总览,它包括每个 test case 以及其中运行函数的名字。我们也可以看到所有发生的失败断言。这些结果都会标注出失败断言发生在测试代码的哪一行。失败的断言也会列出期望值和实际值。
测试结果有一定的格式,这与你用的 IDE 有关。Testing from Eclipse with ADT 中描述了使用带有 ADT 的 Eclipse 测试结果的格式。Testing from Other IDEs 中描述了使用命令行测试结果的格式。
monkey and monkeyrunner
SDK 针对应用程序的功能测试提供了两个工具:
- UI/Application Exerciser Monkey,通常叫 “monkey”。它是一个命令行工具,通过它可以向设备发送由按键、触摸和手势组成的伪随机流。我们通过 Android Debug Bridge (adb) 来运行它。我们通过它对应用进行压力测试,并查看期间发生的错误。我们可以每次启动该工具都使用同一个随机数种子,来重复的发送一个事件流。
- mokeyrunner,它是一个针对 Phython 测试程序的一套 API 和执行环境。该 API 包含了连接设备的功能、包的安装和卸载、屏幕截图、比较图片和运行针对一个应用程序的 test package 的功能。通过这个 API 我们可以写出一个大型的、功能强大的、超级复杂的测试出来。用这个 API 写出的程序可以通过命令行工具 monkeyrunner 来运行。
Working With Package names
在测试环境中,我们既要用到 Android 应用程序包名,有要用到 Java 包标识符。虽然它们格式相同,但实质上他们代表不同的东西。要正确设置测试就要区分它们的不同点。
Android 应用程序包名是一个 apk 文件在系统中唯一的名字,通过 manifest 文件中 <manifest> 标签的 "android:package" 属性来设置它。测试程序的 Android 应用程序包名必须和待测程序的不同。默认情况下, Android tools 把待测程序的包名加上一个 ".test" 做为测试程序的包名。
测试程序也会用一个 Android 应用程序包名来指定它的测试目标,这是通过测试程序 manifest 文件中的 <instrutmentation> 标签的 "android:targetPackage" 属性来设定的。
Java 包标识符适用于源代码文件。这包名反应了源代码文件的路径。它影响这类之间以及成员之间的可见性。
创建测试工程的 Android 工具会为我们设置一个 Android 测试包名。根据我们的输入,该工具会设置一个测试包名和测试目标的包名。要想让这些工具正常运行,(待测)工程必须已经存在。
默认情况下,这些工具会把测试类的 Java 包标识符和 Android 包名设置成一样的。如果我们想通过给它们可性来暴露待测项目中的一些程序出来,你可能需要对其做些修改。如果真要这么干,记得只改动 Java 包标识符,不要改 Android 包名,并且只改动 test case 的源文件。不要修改测试包中产生的 R.java 类的 Java 包标识符,因为它会与待测项目中 R.java 类冲突。不要把测试包的 Android 包名改成和待测项目的一样,因为这样他们名字在系统中就不是唯一的了。
What to Test
What to Test 章节介绍了我们应该测试 Android 应用的哪些关键功能,哪些情况可能会影响到这些功能。
很多单元测试都是针对 Android 组件的。Activity Tesing,Content Provider Tesing 和 Service Testing 这些章节中每个里面都有一个 “What To Test” 部分列出需要测试的范围。
如果可以,最好在真机运行这些测试。否则,也可以根据要测设备的硬件、屏幕、版本,用 Android Emulator 配置一个虚拟设配来测试。
Next Steps
在 Eclipse 中如何设置运行测试,请参考 Tesing from Eclipse with ADT,如果不是用 Eclipse 请参考 Testing from Other IDEs。
如果要一步一步学 Android 测试,请参考 Activity Testing Tuorial。