一、什么是单元测试
单元测试是检查软件设计中小单元正确性的测试。单元,指的是一个应用程序中小的可测部分,在Android/OPhone应用中,我们可以把单元定位为某一个方法。因此,Android/OPhone单元测试是验证代码中每个方法实现正确与否的测试。
单元测试属于白盒测试。因为代码的作者是了解代码结构和内部逻辑的人,因此单元测试好是由开发人员自己完成。
二、单元测试的重要性
单元测试可以帮助开发人员提高代码质量,保证代码实现与设计的一致性,提高代码的健壮性和可测性;
开展单元测试,能将缺陷扼杀在编码阶段。缺陷发现得越早,修改的时间成本和人力成本越低。
功能测试、系统测试等黑盒测试从UI发起,不能保证遍历到所有的代码实现,会导致很多隐藏问题。单元测试能从源头上大限度减少此类问题的发生。
在需求频繁变更导致代码频繁变化时,单元测试可以帮助开发人员避免修改代码引入新的错误
三、Android/OPhone单元测试框架
Android/OPhone平台中整合了JUnit 3测试框架和Instrumentation机制。通过它们,可以对OPhone应用进行有效的单元测试。
四、OPhone单元测试基础
OPhone单元测试的基本方法与一般的基于JUnit的单元测试类似,这里简单介绍下JUnit框架:
JUnit框架为Java单元测试提供了如下功能:
● 断言 (Assertion), 以在测试之前或之后测试预期值。
● 测试固件(fixture),以模拟正在接受测试的代码正常运行所需的环境(包括所有必需的对象)。
● 测试套件(suite),以将各个测试用例组合到一起。
● 测试运行器(runner),以运行测试并捕获和报告测试是成功还是失败。
大多数情况下,创建一组单元测试也是创建从某个JUnit 测试用例类继承的类,并向该类添加新方法以执行各个单元测试。
在JUnit 框架中,Test Case指的是一个包括若干个测试方法的Java类,但实际上,每个测试方法才是我们一般理解中的测试用例。
下面是一个标准JUnit单元测试类的实例。
在ExampleTest例子中
ExampleTest继承自junit.framework.TestCase,setUp()和tearDown()是测试固件,setUp()中设置测试的初始条件,tearDown()负责清除测试环境,保证每个测试用例执行时环境的独立性。
testReading1() 和testReading2()是实际的测试用例,它们的修饰符必须是public,返回值为void,方法名必须以test为前缀。
test方法中的assertNotNull()和assertEqual()都是断言语句,是每个测试用例中的结果校验点。
每个测试用例执行的顺序都是:setUp() -> test() -> tearDown()
在JUnit框架中,可以使用测试套件TestSuite来组织测试用例。下面是一个TestSuite的实例,假设现有ExampleTest1和ExampleTest2两个测试类。
当通过TestRunner运行ExampleTestSuite的时候,包括在ExampleTest1和ExampleTest2中的测试用例都会被执行。
Android/OPhone平台对标准JUnit进行很多扩展和增强,使之更加适合与对OPhone应用进行测试,也可以实现比标准JUnit单元测试更多的功能。例如,从TestCase继承出多个层次的测试类
对于OPhone单元测试,比较常用的子类是AndroidTestCase、ActivityInstrumentationTestCase2和ActivityUnitTestCase。
详细的API说明,可参考Android SDK中的文档。
五、Android/OPhone单元测试实例
Android/OPhone单元测试开发环境和一般的Android/OPhone应用开发环境相同,搭建方法不再赘述。
下面以对OPhone SDK自带的Snake应用进行测试说明OPhone单元测试的步骤
步骤一、导入被测应用。
● 在Eclipse的Navigator窗口选择New -> OPhone Project。
● 在Contents栏中选择Create project from existing source,并点击Browse按钮,之后在浏览窗口中找到Snake应用的源代码所在文件夹(在OPhone SDK安装目录的platforms/android-1.5/samples/apkophoneapps/Snake目录下),选择确定。
● 选择Finish
步骤二、新建测试工程。(步骤二-步骤四在新的ADT中可直接通过创新Android Test Project完成)
● 在Eclipse的Navigator窗口选择New -> OPhone Project。
● 在Contents栏中选择Create new project in workspace。
● Project name填SnakeUnitTest。
● Properties栏中Package name填oms.unittest.snake。
● Create Activity一项前面的复选框可以不选中。
● 后点击Finish。
步骤三、设置测试工程的编译路径。
● 在SnakeUnitTest工程上点击右键,选择Properties。
● 在Properties窗口中选择Java Build Path。
● 切换到Projects标签,点击Add按钮,然后在工程选择窗口勾选被测应用Snake工程。
● 切换到Libraries标签,点击Add Library按钮,在选择窗口选择User Library并点击Next,再点击User Libraries。在User Libraries窗口点击New按钮,填入自定义Library的名字,如OPhone,然后点OK。再选择Add Jars,在弹出窗口切换至OPhone SDK安装目录的platforms/android-1.5目录,选择oms.jar, 后点OK。
步骤四、编辑测试工程的AndroidMenifest.xml。
● 展开SnakeUnitTest,双击AndroidMenifest.xml 。
● 在编辑窗口,将AndroidMenifest.xml 的内容改为如下图所示内容:
● 必须定义;
● android:lable,为可选项,值可以根据项目实际自定义;
● android:name,必须定义,是TestRunner的名字,也可自定义新的TestRunner;
● android:targetPackage,必须定义,对应被测应用的包名。
步骤五、创建测试类。
● 在oms.unittest.snake包下创建SnakeTest.java。每个测试类对应被测应用的一个类,并命名为“被测类名Test”的样式。如Snake应用有Snake、SnakeView和TileView三个类,对应的测试类分别是SnakeTest、SnakeViewTest和TileViewTest。
● SnakeTest.java的框架如下图所示:
在SnakeTest中,包括对com.example.android.snake.Snake中各个方法的测试用例,如testOnPause()是对Snake中的onPause()方法进行测试。“test被测方法”是推荐的单元测试命名方式,这样有助于提高测试代码的可读性。
SnakeTest继承自ActivityInstrumentationTestCase2,是因为被测的Snake.java中定义的是Snake这个Activity的onCreate()和onPause()等,使用ActivityInstrumentationTestCase2的子类可以方便的对这些方法进行测试,并且能得到被测应用和测试应用本身的context和resource。当然,也可以使用InstrumentationTestCase的子类,自主控制Activity的启动和关闭。
下图是TileViewTest的片段:
TileViewTest继承自AndroidTestCase。AndroidTestCase与一般的JUnit TestCase的主要不同之处在于它可以通过getContext()方法得到被测应用的context,但无法得到测试本身的context,也无法获得测试应用自己的resource。当有Res.getXml(R.xml.test)这种访问resource的需要时,必须修改或调用被测应用的resource。这一点需要注意。
下图是为com.example.android.snake.SnakeView写的SnakeViewTest的片段:
如图所示,SnakeViewTest中有一个test01InitialCondition()方法,在这个测试用例中检查了所有在setUp()中定义的初始条件的正确性。这是推荐的测试方式。因为setUp()中如果有错误,会影响所有测试用例的结果。由于JUnit框架是按照test后面的字母排序决定测试方法的执行顺序,因此示例中使用了test01这样的前缀以使该用例第一个被执行。
SnakeViewTest中使用了util.setStatusText()和util.getStatusText()方法。这里的util是测试包中TestUtil类的实例。将测试中用到的一些公用方法放到一个类中管理,有助于提高代码的复用度,降低被测应用代码变化后对测试代码的影响。
下图是TestUtil类的内容:
对于被测类中的protected和private类型的变量和方法,可以用反射的方法访问,如getStatusText() 和setStatusText()。
在一般的JUnit单元测试时,会建议测试代码和被测代码的包名保持一致,以便于访问protected类型的变量和方法。OPhone单元测试代码也可以放在与被测代码同名的包中,方案如下:
1、测试代码放在单独的工程中,该工程的包名与被测代码包名一致;
2、在被测应用代码下新建目录,如tests/src,在该目录下增加与被测代码使用相同包名的测试代码,并将该目录添加到整个工程的编译路径中。
但这两种方案都有一个共同的缺点:需要修改被测应用的代码,如被测应用的AndroidMenifest.xml中必须增加uses-liibray和Instrumentation信息。当需要特殊的测试资源时,方案2还必须修改被测应用的resource资源,并不能得到单独的测试apk。
综合考虑,建议采用前述的独立测试工程+独立包名的方式,并使用反射机制访问非public的变量和方法。
步骤六、创建TestSuite或TestRunner(可选,适用于命令行执行方法)
建议使用TestSuite以灵活配置测试用例的执行。下面是一种常用的TestSuite。如果运行MyTestSuite,则SnakeTest和TileViewTest中的测试用例将被执行。
也可以使用TestRunner来控制运行哪些测试类。如果定义了TestRunner,则需要在测试代码的AndroidMenifest.xml增加相应的Instrumentation定义。
下图是TestRunner的示例和相应的Instrumentation信息
步骤七、执行测试。
执行测试有两种方式:1、通过Eclipse菜单;2、通过命令行。
首先介绍通过Eclipse菜单的方式。如果运行测试工程中所有的测试用例,可以在工程上点击右键,选择Run As -> OPhone JUnit Test。
如果某个测试类,可以展开工程上,在该类上点击右键,选择Run As -> OPhone JUnit Test。
如果单个测试用例(test方法),可以切换到某个测试类的Outline标签,选中一个test方法,点击右键,选择Run As -> OPhone JUnit Test。
也可以使用命令行来执行测试用例。方法如下:
假设SDK的路径已经加入到系统环境变量中,已创建名称为test的avd ,被测应用编译生成为Snake.apk,测试代码编译生成为SnakeUnitTest.apk,且两个apk保存在D盘。
对于Windows系统,打开cmd窗口,执行emulator –avd test启动模拟器。
执行adb install D:Snake.apk和adb install D:SnakeUnitTest.apk安装被测应用和测试包。如遇安装失败,可以先删掉模拟器里已经存在的应用,命令为adb uninstall 。
执行测试命令。
● 运行所有测试:
adb shell am instrument -w oms.unittest.snake /android.test.InstrumentationTestRunner
● 运行某个TestSuite或单个测试类:
adb shell am instrument -e class oms.unittest.snake.MyTestSuite -w oms.unittest.snake/android.test.InstrumentationTestRunner
或 adb shell am instrument -e class oms.unittest.snake.SnakeViewTest -w oms.unittest.snake/android.test.InstrumentationTestRunner
● 运行某个测试方法:
adb shell am instrument -e class oms.unittest.snake.SnakeTest#testOnPause -w oms.unittest.snake/android.test.InstrumentationTestRunner
● 运行某个自定义的Runner:
adb shell am instrument -w oms.unittest.snake / oms.unittest.snake.SnakeUnitTestRunner
步骤八,查看结果。
在Eclipse中,运行后,会有JUnit 标签页,显示运行结果:
命令行模式下的结果
“ . ”表示通过,“F”表示失败,“E”表示出错。
六、单元测试基本原则
进行单元测试请遵循以下基本原则:
哪怕只有一个单元测试用例也比一个都没有好,要坚持进行单元测试。
对被测代码中的每个类和其中的每个方法设计测试用例。
为被测方法里的每个分支设计单独的测试用例。
为被测方法里的每个条件设计单独的测试用例。
对于存在边界值的代码,如某个方法的参数有一定的取值范围,要设计多个单元测试用例覆盖边界和取值范围内的情况。
设计一些反向测试用例,如给一个方法传递一些无效的参数,保证代码能对各种异常进行正确的响应。
对于功能复杂的系统,如某个方法需要和网络服务器进行交互,可以实现一个独立的与应用代码无关的测试功能模块,用于模拟服务器的行为,如为该方法提供响应参数,而不是让该方法真的和网络通信。
当一个bug被修复后,要设计1个或多个单元测试用例来进行验证。
在向代码库提交代码前,要运行一遍单元测试用例以保证没有bug。
不要设计类似于压力测试性质的单元测试用例。
本文转自http://labs.chinamobile.com/mblog/521/171132