Android单元测试介绍
处于高速迭代开发中的Android项目往往需要除黑盒测试外更加可靠的质量保障,这正是单元测试的用武之地。单元测试周期性对项目进行函数级别的测试,在良好的覆盖率下,能够持续维护代码逻辑,从而支持项目从容应对快速的版本更新。
单元测试是参与项目开发的工程师在项目代码之外建立的白盒测试工程,用于执行项目中的目标函数并验证其状态或者结果,其中,单元指的是测试的最小模块,通常指函数。如图1所示的绿色文件夹即是单元测试工程。这些代码能够检测目标代码的正确性,打包时单元测试的代码不会被编译进入APK中。
与Java单元测试相同,Android单元测试也是维护代码逻辑的白盒工程,但由于Android运行环境的不同,Android单元测试的环境配置以及实施流程均有所不同。
Java单元测试
在传统Java单元测试中,我们需要针对每个函数进行设计单元测试用例。如图2便是一个典型的单元测试的用例。
上述示例中,针对函数dosomething(Boolean param)的每个分支,我们都需要构造相应的参数并验证结果。单元测试的目标函数主要有三种:
- 有明确的返回值,如上图的dosomething(Boolean param),做单元测试时,只需调用这个函数,然后验证函数的返回值是否符合预期结果。
- 这个函数只改变其对象内部的一些属性或者状态,函数本身没有返回值,就验证它所改变的属性和状态。
- 一些函数没有返回值,也没有直接改变哪个值的状态,这就需要验证其行为,比如点击事件。
既没有返回值,也没有改变状态,又没有触发行为的函数是不可测试的,在项目中不应该存在。当存在同时具备上述多种特性时,本文建议采用多个case来真对每一种特性逐一验证,或者采用一个case,逐一执行目标函数并验证其影响。
构造用例的原则是测试用例与函数一对一,实现条件覆盖与路径覆盖。Java单元测试中,良好的单元测试是需要保证所有函数执行正确的,即所有边界条件都验证过,一个用例只测一个函数,便于维护。在Android单元测试中,并不要求对所有函数都覆盖到,像Android SDK中的函数回调则不用测试。
Android单元测试
在Android中,单元测试的本质依旧是验证函数的功能,测试框架也是JUnit。在Java中,编写代码面对的只有类、对象、函数,编写单元测试时可以在测试工程中创建一个对象出来然后执行其函数进行测试,而在Android中,编写代码需要面对的是组件、控件、生命周期、异步任务、消息传递等,虽然本质是SDK主动执行了一些实例的函数,但创建一个Activity并不能让它执行到resume的状态,因此需要JUnit之外的框架支持。
当前主流的单元测试框架AndroidTest和Robolectric,前者需要运行在Android环境上,后者可以直接运行在JVM上,速度也更快,可以直接由Jenkins周期性执行,无需准备Android环境。因此我们的单元测试基于Robolectric。对于一些测试对象依赖度较高而需要解除依赖的场景,我们可以借助Mock框架。
Android单元测试环境配置
Robolectric环境配置
Android单元测试依旧需要JUnit框架的支持,Robolectric只是提供了Android代码的运行环境。如果使用Robolectric 3.0,依赖配置如下:
1 2 3 |
testCompile 'junit:junit:4.10' testCompile 'org.robolectric:robolectric:3.0' |
Gradle对Robolectric 2.4的支持并不像3.0这样好,但Robolectric 2.4所有的测试框架均在一个包里,另外参考资料也比较丰富,作者更习惯使用2.4。如果使用Robolectric 2.4,则需要如下配置:
1 2 3 4 |
classpath 'org.robolectric:robolectric-gradle-plugin:0.14.+'//这行配置在buildscript的dependencies中 apply plugin: 'robolectric' androidTestCompile 'org.robolectric:robolectric:2.4' |
上述配置中,本文将testCompile写成androidTest,并且常见的Android工程的单元测试目录名称有test也有androidTest,这两种写法并没有功能上的差别,只是Android单元测试Test Artifact不同而已。Test Artifact如图3所示:
在Gradle插件中,这两种Artifact执行的Task还是有些区别的,但是并不影响单元测试的写法与效果。虽然可以主动配置单元测试的项目路径,本文依旧建议采用与Test Artifact对应的项目路径和配置写法。
Mock配置
如果要测试的目标对象依赖关系较多,需要解除依赖关系,以免测试用例过于复杂,用Robolectric的Shadow是个办法,但是推荐更加简单的Mock框架,比如Mockito,该框架可以模拟出对象来,而且本身提供了一些验证函数执行的功能。Mockito配置如下:
1 2 3 4 5 6 7 |
repositories { jcenter() } dependencies { testCompile "org.mockito:mockito-core:1.+" } |
Robolectric使用介绍
Robolectric单元测试编写结构
单元测试代码写在项目的test(也可能是androidTest,该目录在项目中会呈浅绿色)目录下。单元测试也是一个标准的Java工程,以类为文件单位编写,执行的最小单位是函数,测试用例(以下简称case)是带有@Test注解的函数,单元测试里面带有case的类由Robolectric框架执行,需要为该类添加注解@RunWith(RobolectricTestRunner.class)。基于Robolectric的代码结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//省略一堆import @RunWith(RobolectricTestRunner.class) public class MainActivityTest { @Before public void setUp() { //执行初始化的操作 } <a href="http://www.jobbole.com/members/madao">@Test</a> public void testCase() { //执行各种测试逻辑判断 } } |
上述结构中,带有@Before注解的函数在该类实例化后,会立即执行,通常用于执行一些初始化的操作,比如构造网络请求和构造Activity。带有@test注解的是单元测试的case,由Robolectric执行,这些case本身也是函数,可以在其他函数中调用,因此,case也是可以复用的。每个case都是独立的,case不会互相影响,即便是相互调用也不会存在多线程干扰的问题。
常见Robolectric用法
Robolectric支持单元测试范围从Activity的跳转、Activity展示View(包括菜单)和Fragment到View的点击触摸以及事件响应,同时Robolectric也能测试Toast和Dialog。对于需要网络请求数据的测试,Robolectric可以模拟网络请求的response。对于一些Robolectric不能测试的对象,比如ConcurrentTask,可以通过自定义Shadow的方式现实测试。下面将着重介绍Robolectric的常见用法。
Robolectric 2.4模拟网络请求
由于商业App的多数Activity界面数据都是通过网络请求获取,因为网络请求是大多数App首要处理的模块,测试依赖网络数据的Activity时,可以在@Before标记的函数中准备网络数据,进行网络请求的模拟。准备网络请求的代码如下:
1 2 3 4 5 6 |
public void prepareHttpResponse(String filePath) throws IOException { String netData = FileUtils.readFileToString(FileUtils. toFile(getClass().getResource(filePath)), HTTP.UTF_8); Robolectric.setDefaultHttpResponse(200, netData); }//代码适用于Robolectric 2.4,3.0需要注意网络请求的包的位置 |
由于Robolectric 2.4并不会发送网络请求,因此需要本地创建网络请求所返回的数据,上述函数的filePath便是本地数据的文件的路径,setDefaultHttpResponse()则创建了该请求的Response。上述函数执行后,单元测试工程便拥有了与本地数据数据对应的网络请求,在这个函数执行后展示的Activity便是有数据的Activity。
在Robolectric 3.0环境下,单元测试可以发真的请求,并且能够请求到数据,本文依旧建议采用mock的办法构造网络请求,而不要依赖网络环境。
Activity展示测试与跳转测试
创建网络请求后,便可以测试Activity了。测试代码如下:
1 2 3 4 5 6 7 8 |
<a href="http://www.jobbole.com/members/madao">@Test</a> public void testSampleActivity(){ SampleActivity sampleActivity=Robolectric.buildActivity(SampleActivity.class). create().resume().get(); assertNotNull(sampleActivity); assertEquals("Activity的标题", sampleActivity.getTitle()); } |
Robolectric.buildActivity()用于构造Activity,create()函数执行后,该Activity会运行到onCreate周期,resume()则对应onResume周期。assertNotNull和assertEquals是JUnit中的断言,Robolectric只提供运行环境,逻辑判断还是需要依赖JUnit中的断言。
Activity跳转是Android开发的重要逻辑,其测试方法如下:
1 2 3 4 5 6 |
<a href="http://www.jobbole.com/members/madao">@Test</a> public void testActivityTurn(ActionBarActivity firstActivity, Class secondActivity) { Intent intent = new Intent(firstActivity.getApplicationContext(), secondActivity); assertEquals(intent, Robolectric.shadowOf(firstActivity).getNextStartedActivity());//3.0的API与2.4不同 } |
Fragment展示与切换
Fragment是Activity的一部分,在Robolectric模拟执行Activity过程中,如果触发了被测试的代码中的Fragment添加逻辑,Fragment会被添加到Activity中。
需要注意Fragment出现的时机,如果目标Activity中的Fragment的添加是执行在onResume阶段,在Activity被Robolectric执行resume()阶段前,该Activity中并不会出现该Fragment。采用Robolectric主动添加Fragment的方法如下:
1 2 3 4 5 6 7 |
<a href="http://www.jobbole.com/members/madao">@Test</a> public void addfragment(Activity activity, int fragmentContent){ FragmentTestUtil.startFragment(activity.getSupportFragmentManager().findFragmentById(fragmentContent)); Fragment fragment = activity.getSupportFragmentManager().findFragmentById(fragmentContent); assertNotNull(fragment); } |
startFragment()函数的主体便是常用的添加fragment的代码。切换一个Fragment往往由Activity中的代码逻辑完成,需要Activity的引用。
控件的点击以及可视验证
1 2 3 4 5 6 7 8 |
<a href="http://www.jobbole.com/members/madao">@Test</a> public void testButtonClick(int buttonID){ Button submitButton = (Button) activity.findViewById(buttonID); assertTrue(submitButton.isEnabled()); submitButton.performClick(); //验证控件的行为 } |
对控件的点击验证是调用performClick(),然后断言验证其行为。对于ListView这类涉及到Adapter的控件的点击验证,写法如下:
1 2 3 |
//listView被展示之后 listView.performItemClick(listView.getAdapter().getView(position, null, null), 0, 0); |
与button等控件稍有不同。
Dialog和Toast测试
测试Dialog和Toast的方法如下:
1 2 3 4 5 6 7 8 9 |
public void testDialog(){ Dialog dialog = ShadowDialog.getLatestDialog(); assertNotNull(dialog); } public void testToast(String toastContent){ ShadowHandler.idleMainLooper(); assertEquals(toastContent, ShadowToast.getTextOfLatestToast()); } |
上述函数均需要在Dialog或Toast产生之后执行,能够测试Dialog和Toast是否弹出。
Shadow写法介绍
Robolectric的本质是在Java运行环境下,采用Shadow的方式对Android中的组件进行模拟测试,从而实现Android单元测试。对于一些Robolectirc暂不支持的组件,可以采用自定义Shadow的方式扩展Robolectric的功能。
1 2 3 4 5 6 7 8 9 |