Android开发指南(39) —— Testing Fundamentals
前言
本章内容为Android开发者指南的Framework Topics/Testing/Testing Fundamentals章节,版本为Android 4.0 r1,翻译来自:"CodingMyWorld",欢迎访问他的博客:"http://www.cnblogs.com/codingmyworld/",再次感谢"CodingMyWorld" !期待你一起参与翻译Android的相关资料,联系我over140@gmail.com。
测试基础
译者署名:CodingMyWorld
译者链接:http://www.cnblogs.com/codingmyworld/
版本:Android 4.0 r1
原文
http://developer.android.com/guide/topics/testing/testing_android.html
Android测试框架作为整个开发环境不可分割的一部分,提供了一个架构和一些强大的工具,来帮助你在从单元到框架的各个水平上测试你应用的各个方面。
这个测试框架包含了以下主要特性:
* Android测试套件是基于JUnit的。你可以使用纯的JUnit来测试没有调用Android API的类,你也可以使用Android的JUnit扩展来测试Android的组件。如果你在Android测试方面还是新手,你可以先开始用诸如AndroidTestCase这样的通用类,然后在进一步探索其他更复杂的类。
* Android的JUnit扩展提供了特定组件的测试用例类。这些类提供了创建模拟对象的帮助方法,还有帮助你控制相应组件生命周期的方法。
* 测试套件被包含在与主应用程序包类似的测试包中,因此你不需要为设计和构建测试再学习新的工具集和技术。
* 构建和测试的SDK工具在Eclipse的ADT中已经提供了,如果你使用其他的IDE,也能以命令行的形式使用。这些工具从待测应用项目中获取信息,并利用这些信息自动的创建构建文件,清单文件,还有测试包的目录结构。
* SDK也提供monkeyrunner,一套使用Python程序测试设备的API,还有UI/Application Exerciser Monkey,这是一个通过向设备发送伪随机事件从而对UI进行压力测试的命令行工具。
这篇文档描述了Android测试框架的基本原理,包括测试结构,用于开发测试的API,还有你用来运行测试和查看结果的工具。此文档假定你已经掌握了Android应用开发的基本知识和使用JUnit测试的方法。
下面这张图总结了整个测试框架:
测试结构(Test Structure)
Android的构建与测试工具设想测试项目被组织成一个标准的测试结构,及测试用例类、测试包、还有测试项目。
Android的测试是基于JUnit的。JUnit的测试大体上就是一个方法,由方法中的语句对待测试应用的某一部分进行测试。你将这些测试方法组织在一起,放到一个叫做测试用例(或者测试套件)的类中。每个测试在待测试应用的单个模块中都是独立的。每个测试用例类都是相关测试方法的一个容器,尽管它也提供一些帮助方法。
在JUnit中,一个或多个测试源文件被构建到class文件中;在Android中,你同样使用SDK的构建工具将一个或多个测试源文件构建到Android测试包中的class文件里。在JUnit中,你使用测试运行器来执行测试用例;在Android中,你使用测试工具加载测试包和待测应用,然后由工具来执行特定的Android测试运行器。
测试项目(Test Projects)
测试,和Android应用程序一样,被组织成项目。
一个测试项目是一个目录或者Eclipse工程,你在其中创建源代码、清单文件、还有测试包下的其他文件。Android SDK包含了为你创建和更新测试项目的工具,既有在Eclipse ADT中使用的,又有在命令行中使用的。这些工具为你创建存放源代码和资源文件的目录以及测试包的清单文件。命令行工具也会创建你需要的Ant构建文件。
你应该总是使用Android的工具来创建测试项目。工具还有这些好处:
* 自动设置你的测试包使用InstrumentationTestRunner作为测试用例的运行器。你必须使用InstrumentationTestRunner(或者它的子类)来运行JUnit测试。
* 为测试包创建合适的名字。如果待测试应用有一个叫做com.mydomain.myapp的包名,那么Android工具会将测试包命名成com.mydomain.myapp.test。这帮助你辨认它们之间的关系,防止系统内的冲突。
* 自动创建恰当的构建文件、清单文件、和测试项目的目录结构。这帮助你在构建测试包的时候无需手动修改构建文件和设置测试包与待测应用之间的联系。
你可以在文件系统的任何地方创建测试项目,但是最佳的方式是将你的测试项目添加到主应用程序项目中,这样测试项目的根目录tests/就和主应用程序的src/目录在同一个目录层次上了。这帮助你很快的找到与应用相关的测试。例如,如果你的应用程序项目的根目录是MyProject,那么你应该使用如下的目录结构:
MyProject/
AndroidManifest.xml
res/
... (主应用程序的资源)
src/
... (主应用程序的代码) ...
tests/
AndroidManifest.xml
res/
... (测试使用的资源)
src/
... (测试使用的代码)
测试API(The Testing API)
Android测试API基于JUnit的API,并且扩展了一套仪器框架(instrumentation framework)和一些特定的Android测试类。
JUnit
你可以在普通的Java对象上使用JUnit的TestCase类来做单元测试。TestCase类也是AndroidTestCase类的父类,你可以使用AndroidTestCase类来测试依赖Android的对象。AndroidTestCase类除了提供JUnit框架中拥有的功能,也提供了专门在Android上使用的setup,teardown和帮助方法。
你使用JUnit中的Assert类来显示测试结果。assert方法将你在测试中期望的结果与实际的结果作比较,并且在比较失败的时候抛出异常。Android也提供了断言类,它继承了JUnit Assert类中的各种断言类型,还提供了另一类用于测试界面的断言。这些内容在断言类(Assertion classes)一节中有详细描述。
想了解更多有关JUnit的内容,你可以阅读junit.org主页上文档。注意Android测试API仅支持JUnit 3的编码风格,不支持JUnit 4。你也必须使用Android的测试运行器InstrumentationTestRunner来运行你的测试用类。测试运行器在运行测试(Running Tests)一节中探讨。
仪器(Instrumentation)
在Android系统中,仪器是指一组控制方法或者“钩子”。这些钩子控制着Android组件,使其独立于它正常的生命周期。它们也控制着Android如何加载应用程序。
正常情况下,Android组件运行在由系统决定的生命周期中。比如,一个Activity对象的生命周期在它被一个Intent激活时开始,它的onCreate()方法被调用,然后是onResume()。当用户启动另一个应用,onPause()方法被调用。如果Activity的代码调用了finish()方法,那么onDestroy()方法会被调用。Android的框架API不提供让你直接调用这些回调方法的方式,但是你可以使用仪器(instrumentation)来做这些。
同样,系统把一个应用程序中的所有组件运行在同一个进程中。你可以允许一些组件,如content provider,运行在一个独立的进程中,但是你不能强制一个应用程序和一个已经运行的应用程序运行在同一个进程中。
然而,有了Android仪器,你可以在测试代码中调用那些回调方法。这让你可以一步一步地遍历运行一个组件的生命周期,仿佛你在调试组件。下面的代码片段演示了如何使用仪器来测试一个Activity保存和恢复状态:
// 开始待测应用的主activity
mActivity= getActivity();
// 得到该activity对象一个主界面控件的句柄,一个Spinner
mSpinner= (Spinner)mActivity.findViewById(com.android.example.spinner.R.id.Spinner01);
// 设置Spinner的已知未知
mActivity.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(),它是仪器API的一部分。待测试的Activity直到该方法被调用的时候才会启动。你可以提前设置一些测试配置,然后再调用该方法来启动Activity。
同样,仪器能将测试包和待测应用加载到同一个进程中。既然应用组件与它们的测试程序在同一个进程中,测试程序就能调用组件的方法并且修改和检查组件中的属性了。
测试用例类
Android提供了一些测试用例类,它们都继承自TestCase类和Assert类,并带有Android特定的setup,teardown和帮助方法。
AndroidTestCase类
AndroidTestCase类是一个有用的通用类,特别是在你刚开始接触Android测试的时候。它继承了TestCase类和Assert类,提供了JUnit标准的setUp()和tearDown()方法,还有所有的JUnit里的Assert方法。此外,它还提供了测试许可(permission)的方法和通过清除一些类引用来防止内存泄露的方法。
特定组件的测试用例(Component-specific test cases)
Android测试框架的一个关键特性就是它的特定组件测试类。这些类提供了安装(setup)和卸载(teardown)测试所需资源(fixture)的方法和控制组件生命周期的方法,解决了对特定组件进行测试的需求。它们还提供了创建模拟对象的方法。我们在特定组件测试的章节中讨论这些类:
Android没有为BroadcastReceiver提供一个独立的测试用例类,取而代之的是通过向注册BroadcastReceiver的组件发送一个intent对象,判断broadcast receiver能否正确响应来进行测试的。
应用测试用例(ApplicationTestCase)
你使用ApplicationTestCase类来测试Application对象的setup和teardown。这些对象维护着一个应用包中的所有组件都使用的全局状态信息。这个测试用例在验证清单文件中<application>元素是否被正确设置(setup)时是很有用的。但是注意该测试用例不允许你进行应用包中组件的测试。
仪器测试用例(InstrumentationTestCase)
如果你想在测试用例中用到仪器方法,你必须使用InstrumentationTestCase类或它的一个子类。Activity测试用例继承了该基类并实现了其他功能帮助Activity测试。
断言类(Assertion classes)
由于Android测试用例类继承自JUnit,你可以使用断言方法来显示测试的结果。断言方法的作用是将测试返回的实际结果同你期望的结果作比较,并在比较失败的时候抛出AssertionException。使用断言比记录日志更便利,能提供更好的测试效果。
除了JUnit的Assert类方法,测试API还提供了MoreAsserts和ViewAsserts类:
* MoreAsserts类包含了更强大的断言,比如assertContainsRegex(String, String)方法,它能够匹配正则表达式。
* ViewAsserts类包含测试视图时有用的断言。例如assertHasScreenCoordinates(View, View, int, int)方法,该方法能测试一个视图在可视屏幕特定的(X,Y)坐标处是否有位置。这些断言方法简化了对界面几何形状和对齐方式的测试。
模拟对象类(Mock Object classes)
为了在测试中方便地使用依赖注入,Android提供了创建模拟系统对象的类,比如Context对象,ContentProvider对象,ContentResolver对象和Service对象。一些测试用例还提供了Intent模拟对象。你使用这些对象既是为了将测试与系统中的其他测试隔离开来,又能很方便的在测试中使用依赖注入。这些类能在Java包android.test和android.test.mock中找到。
模拟对象通过“屏蔽”(stub out)或重写常规方法将测试同正在运行的系统隔离开。例如,MockContentResolver使用自己的本地框架替代常规的Resolver框架,这与系统的其他部分是隔离的。MockContentResolver也屏蔽了notifyChange(Uri, ContentObserver, boolean)方法,这样测试环境外的观察者对象就不会被不小心触发了。
模拟对象还通过提供常规对象的子类来方便依赖注入,这些模拟对象并没有实际的功能,除非你重写了父类的方法。例如,MockResources对象是Resources的子类,里面所有的方法在调用时都会抛出异常。使用它的时候,你只需重写必须提供信息的方法。
下面这些是Android中可用的模拟对象:
简单模拟对象类(Simple mock object classes)
MockApplication,MockContext,MockContentProvider,MockCursor,MockDialogInterface,MockPackageManager和MockResources提供了简单有用的模拟策略。他们是系统中相应对象的空实现(stubbed-out)版本,其中所有的方法在被调用的时候会抛出UnsupportedOperationException。使用它们的时候,你重写需要的方法来依赖模拟。
注意:MockContentProvider和MockCursor是API level 8中新添的。
Resolver模拟对象(Resolver mock objects)
MockContentResolver通过屏蔽掉系统的常规resolver框架,提供了对content providers的隔离测试。MockContentResolver使用自己的内部表,而不是提供一个authority string去系统内部查找content provider。你必须显示地使用addProvider(String, ContentProvider)方法为内部表增加provider。
有了这个功能,你能将一个模拟的content provider与一个authority关联起来。你能创建一个真实provider的实例,但是仅使用其中的测试数据。你甚至可以设置一个authority的provider为null。实际上,一个MockContentResolver对象将你的测试同包含真实数据的provider隔离开来了。你可以控制provider的功能,你可以防止测试影响到真实的数据。
测试的上下文(Contexts for testing)
Android提供了两个上下文类用于测试:
* IsolatedContext提供了一个隔离的上下文(Context),使用该上下文的文件、目录、数据库操作将发生在测试空间。尽管它的功能受限,该上下文还是有足够的桩代码(stub code)响应系统调用。
该类允许你测试应用的数据操作,而且不会影响到可能在设备上存在的真实数据。
* RenamingDelegatingContext提供了一个上下文,其中大多数的操作都由一个已存在的上下文处理,但是文件和数据库的操作由一个IsolatedContext处理。隔离部分使用测试目录,并创建特殊的文件和目录名。你可以自己控制它们的命名,或者让构造器自动决定。
该对象提供了一种为数据操作设置隔离区的快速方式,并且保留了对其他上下文操作的常规功能。
运行测试(Running Tests)
测试用例是通过测试运行器类运行的,测试运行器先加载测试用例类,然后安装(set up)测试资源,运行测试用例,最后卸载(tear down)每个测试的资源。Android的测试运行器还必须装上测试仪器(instrumentation),以便启动应用的系统工具能控制测试包(test package)如何加载测试用例和待测应用程序。你通过在测试包的清单文件中设置一个值来告诉android平台使用哪个装好仪器的测试运行器。
InstrumentationTestRunner是主要的测试运行器。它继承了JUnit的测试运行器框架并且装上了测试仪器。它能运行Android提供的所有测试用例类,并支持所有可能的测试种类。
你在测试包的清单文件的instrumentation元素中指定InstrumentationTestRunner或它的子类作为你的测试运行器。InstrumentationTestRunner的代码在共享库android.test.runner中,该库正常情况下没有被连接到Android代码中。你必须在uses-library元素中指定包含它。你不需要自己手动设置这些元素,Eclipse的ADT工具和android命令行工具会在你的测试包清单文件中自动构建他们。
注意:如果你要使用别的测试运行器而不是InstrumentationTestRunner,你必须改变<instrumentation>元素,使它指向你要用的类。
你要使用被Android工具调用的内部系统类来运行InstrumentationTestRunner。当你在装有ADT的Eclipse中运行测试时,这些类会被自动调用。当你从命令行中运行测试时,你通过Android Debug Bridge(adb)工具运行它们。
这些系统类加载和启动测试包时,先杀死所有正在运行的待测应用的进程,然后加载待测应用的新进程。然后它们将控制交给InstrumentationTestRunner,由它来运行测试包下的每个测试用例。你还可以使用Eclipse ADT插件中的设置选项(settings)和命令行工具的标志位(flag)来控制运行哪些测试用例和方法。
待测应用既不是由系统类启动,也不是由InstrumentationTestRunner启动,而是由测试用例直接启动的。测试用例既可以调用待测应用的方法,又可以调用自己的方法,这些方法能在待测应用中触发生命周期时间的事件。待测应用在测试用例的完全控制之下,这就允许测试用例在运行测试之前设置(set up)测试环境(测试fixture)。这在先前对一个显示Spinner控件的Activity的测试代码段中已经演示了。
想了解更多有关运行测试的内容,请阅读使用ADT在Eclipse中测试或者在其他IDE中测试的文章。
查看测试结果(Seeing Test Results)
Android测试框架将测试结果返回到运行测试的工具上。如果你在Eclipse中使用ADT来运行测试,那么测试结果显示在一个新的Junit视图窗格种。如果你从一个命令行中运行测试,那么结果显示在STDOUT中。在两种情况下,你都会看到一个测试总结显示每个测试用例的名字和其中运行的方法。你还能看到所有发生的断言失败,它们包括了出错行的指针。断言失败还列出了期望值和实际值。
测试结果有特点的格式,这与你使用的IDE有关。使用Eclipse ADT的测试结果格式在使用ADT在Eclipse中测试中描述。使用命令行的测试结果格式在在其他IDE中测试中描述。
monkey和monkeyrunner
SDK针对功能级别的测试提供了两个工具:
* UI/Application Exerciser Monkey,通常被叫做“monkey”,是一个向设备发送键盘敲击、触摸、手势等伪随机流的命令行工具。你使用Android Debug Bridge(adb)来启动它。你使用它来对你的程序作压力测试并报道遇到的错误。你可以通过每次使用相同的随机数种子启动该工具来重复一个事件流。
* monkeyrunner工具是使用Python来测试程序的一套API和执行环境。这套API包括了连接设备、安装和卸载应用包、截屏、比较两幅图片和运行测试应用测试包的功能。有了monkeyrunner命令行工具提供的这套API,你可以写出大型的、强大的、复杂的测试。
使用包名工作(Working With Package names)
在测试环境中,你会同时遇到Android应用程序的包名和Java的包名。这两个包名的命名格式相同,但它们实质上代表了不同的实体。你需要知道这些不同来帮助你正确地设置你的测试。
Android的包名是一个.apk文件独一无二的系统名,它由包内清单文件中的<manifest>元素的"android:package"属性设置。你测试包的Android包名必须和待测应用的包名不同。默认情况下,Android工具在待测应用包名的后面加上".test"来作为测试包名。
测试包同样使用Android包名来找到它所测试的应用程序包。这是在测试包清单文件的<instrumentation>元素的"android:targetPackage"属性上设置的。
Java的包名符适用于源文件。这个包名反映了源文件的目录路径。它也影响着类之间、成员之间的可见性。
创建测试项目的Android工具为你设置Android测试包的包名。工具会根据你的输入来设置测试包的包名和待测应用的包名。要想让这些工具工作,必须已经有应用项目了。
默认情况下,这些工具把测试类的Java包名与测试包的Android包名设置成一样的。如果你想要给它们包可见性,从而在待测应用中暴露出一些成员,你可能需要做些改动。假如你真要这么做了,请你只改动Java的包名,不要改动Android包名,并且只改变测试用例类的源文件。不要改变在你测试包中生成的R.java类的Java包名。不要将测试包的Android包名改成与待测应用的Android包名一样,因为这样的话系统中的Android包名就不是唯一的了。
测试什么(What to Test)
测试什么一章讨论了你应该在Android应用程序中测试的关键功能和可能影响功能的关键情形。
大多数单元测试都特定在你正在测试的Android组件上。Activity Testing,Content Provider Testing和Service Testing每一篇都有题为“测试什么”的小节,列出了可能的测试范围。
可能的话,你应该在真实设备上运行这些测试。不可能的话,你可以使用针对你所测试的硬件、屏幕、版本配置的Android虚拟设备的Android模拟器。
下一步
学习如何在Eclipse中运行测试,请参考使用ADT在Eclipse中测试。如果你不使用Eclipse工作,请参考在其他IDE中测试。
如果你想要一个对Android测试的一步一步地介绍,尝试一篇测试教程或者示例测试包:
* Hello, Testing教程介绍了在Hello,World应用上下文中的基本测试概念和步骤。
* Activity Testing教程是Hello,Testing教程的一个很好的后续。它指导你在开发一个更加实际的应用时应该如何完成一个复杂的测试场景。