|
Fragment展示与切换
Fragment是Activity的一部分,在Robolectric模拟执行Activity过程中,如果触发了被测试的代码中的Fragment添加逻辑,Fragment会被添加到Activity中。
需要注意Fragment出现的时机,如果目标Activity中的Fragment的添加是执行在onResume阶段,在Activity被Robolectric执行resume()阶段前,该Activity中并不会出现该Fragment。采用Robolectric主动添加Fragment的方法如下:
@Testpublic 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的引用。
控件的点击以及可视验证
@Testpublic void testButtonClick(int buttonID){ Button submitButton = (Button) activity.findViewById(buttonID); assertTrue(submitButton.isEnabled()); submitButton.performClick(); //验证控件的行为}
对控件的点击验证是调用performClick(),然后断言验证其行为。对于ListView这类涉及到Adapter的控件的点击验证,写法如下:
//listView被展示之后listView.performItemClick(listView.getAdapter().getView(position, null, null), 0, 0);
与button等控件稍有不同。
Dialog和Toast测试
测试Dialog和Toast的方法如下:
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的功能。
@Implements(Point.class)public class ShadowPoint { @RealObject private Point realPoint; ... public void __constructor__(int x, int y) { realPoint.x = x; realPoint.y = y; }}//样例来源于Robolectric官网
上述实例中,@Implements是声明Shadow的对象,@RealObject是获取一个Android 对象,constructor则是该Shadow的构造函数,Shadow还可以修改一些函数的功能,只需要在重载该函数的时候添加@Implementation,这种方式可以有效扩展Robolectric的功能。
Shadow是通过对真实的Android对象进行函数重载、初始化等方式对Android对象进行扩展,Shadow出来的对象的功能接近Android对象,可以看成是对Android对象一种修复。自定义的Shadow需要在config中声明,声明写法是@Config(shadows=ShadowPoint.class)。
Mock写法介绍
对于一些依赖关系复杂的测试对象,可以采用Mock框架解除依赖,常用的有Mockito。例如Mock一个List类型的对象实例,可以采用如下方式:
List list = mock(List.class); //mock得到一个对象,也可以用@mock注入一个对象
所得到的list对象实例便是List类型的实例,如果不采用mock,List其实只是个接口,我们需要构造或者借助ArrayList才能进行实例化。与Shadow不同,Mock构造的是一个虚拟的对象,用于解耦真实对象所需要的依赖。Mock得到的对象仅仅是具备测试对象的类型,并不是真实的对象,也就是并没有执行过真实对象的逻辑。
Mock也具备一些补充JUnit的验证函数,比如设置函数的执行结果,示例如下:
When(sample.dosomething()).thenReturn(someAction);//when(一个函数执行).thenReturn(一个可替代真实函数的结果的返回值);//上述代码是设置sample.dosomething()的返回值,当执行了sample.dosomething()这个函数时,就会得到someAction,从而解除了对真实的sample.dosomething()函数的依赖
上述代码为被测函数定义一个可替代真实函数的结果的返回值。当使用这个函数后,这个可验证的结果便会产生影响,从而代替函数的真实结果,这样便解除了对真实函数的依赖。
同时Mock框架也可以验证函数的执行次数,代码如下:
List list = mock(List.class); //Mock得到一个对象list.add(1); //执行一个函数verify(list).add(1); //验证这个函数的执行verify(list,time(3)).add(1); //验证这个函数的执行次数
在一些需要解除网络依赖的场景中,多使用Mock。比如对retrofit框架的网络依赖解除如下:
//代码参考了参考文献[3]public class MockClient implements Client { @Override public Response execute(Request request) throws IOException { Uri uri = Uri.parse(request.getUrl()); String responseString = ""; if(uri.getPath().equals("/path/of/interest")) { responseString = "返回的json1";//这里是设置返回值 } else { responseString = "返回的json2"; } return new Response(request.getUrl(), 200, "nothing", Collections.EMPTY_LIST, new TypedByteArray("application/json", responseString.getBytes())); }}//MockClient使用方式如下:RestAdapter.Builder builder = new RestAdapter.Builder();builder.setClient(new MockClient());
这种方式下retrofit的response可以由单元测试编写者设置,而不来源于网络,从而解除了对网络环境的依赖。
在实际项目中使用Robolectric构建单元测试
单元测试的范围
在Android项目中,单元测试的对象是组件状态、控件行为、界面元素和自定义函数。本文并不推荐对每个函数进行一对一的测试,像onStart()、onDestroy()这些周期函数并不需要全部覆盖到。商业项目多采用Scrum模式,要求快速迭代,有时候未必有较多的时间写单元测试,不再要求逐个函数写单元测试。
本文单元测试的case多来源于一个简短的业务逻辑,单元测试case需要对这段业务逻辑进行验证。在验证的过程中,开发人员可以深度了解业务流程,同时新人来了看一下项目单元测试就知道哪个逻辑跑了多少函数,需要注意哪些边界——是的,单元测试需要像文档一样具备业务指导能力。
在大型项目中,遇到需要改动基类中代码的需求时,往往不能准确快速地知道改动后的影响范围,紧急时多采用创建子类覆盖父类函数的办法,但这不是长久之计,在足够覆盖率的单元测试支持下,跑一下单元测试就知道某个函数改动后的影响,可以放心地修改基类。
美团的Android单元测试编写流程如图4所示。
图4 美团Android单元测试编写流程
单元测试最终需要输出文档式的单元测试代码,为线上代码提供良好的代码稳定性保证。
单元测试的流程
实际项目中,单元测试对象与页面是一对一的,并不建议跨页面,这样的单元测试藕合度太大,维护困难。单元测试需要找到页面的入口,分析项目页面中的元素、业务逻辑,这里的逻辑不仅仅包括界面元素的展示以及控件组件的行为,还包括代码的处理逻辑。然后可以创建单元测试case列表(列表用于纪录项目中单元测试的范围,便于单元测试的管理以及新人了解业务流程),列表中记录单元测试对象的页面,对象中的case逻辑以及名称等。工程师可以根据这个列表开始写单元测试代码。
单元测试是工程师代码级别的质量保证工程,上述流程并不能完全覆盖重要的业务逻辑以及边界条件,因此,需要写完后,看覆盖率,找出单元测试中没有覆盖到的函数分支条件等,然后继续补充单元测试case列表,并在单元测试工程代码中补上case。
直到规划的页面中所有逻辑的重要分支、边界条件都被覆盖,该项目的单元测试结束。单元测试流程如图5所示。
图5 单元测试执行流程
上述分析页面入口所得到结果便是@Before标记的函数中的代码,之后的循环便是所有的case(@Test标记的函数)。
单元测试项目实践
为了系统的介绍单元测试的实施过程,本文创建了一个小型的demo项目作为测试对象。demo的功能是供用户发布所见的新闻到服务端,并浏览所有已经发表的新闻,是个典型的自媒体应用。该demo的开发和测试涉及到TextView、EditView、ListView、Button以及自定义View,包含了网络请求、多线程、异步任务以及界面跳转等。能够为多数商业项目提供参照样例。项目页面如图6所示。
图6 单元测试case设计
首先需要分析App的每个页面,针对页面提取出简短的业务逻辑,提取出的业务逻辑如图6绿色圈图所示。根据这些逻辑来设计单元测试的case(带有@Test注解的那个函数),这里的业务逻辑不仅指需求中的业务,还包括其他需要维护的代码逻辑。业务流程不允许跨页面,以免增加单元测试case的维护成本。针对demo中界面的单元测试case设计如下:
接下来需要在单元测试工程中实现上述case,最小断言数是业务逻辑上的判断,并不是代码的边界条件,真实的case需要考虑代码的边界条件,比如数组为空等条件,因此,最终的断言数量会大于等于最小断言数。在需求业务上,最小断言数也是该需求的业务条件。
写完case后需要跑一遍单元测试并检查覆盖率报告,当覆盖率报告中缺少有些单元测试case列表中没有但是实际逻辑中会有的逻辑时,需要更新单元测试case列表,添加遗漏的逻辑,并将对应的代码补上。直到所有需要维护的逻辑都被覆盖,该项目中的单元测试才算完成。单元测试并不是QA的黑盒测试,需要保证对代码逻辑的覆盖。
对表1分析,第一个页面的“发布新闻”的case可以直接调用“编写新闻”的case,以满足条件“2.编写了新闻的前提下,点击发布按钮”,在JUnit框架下,case(带@Test注解的那个函数)也是个函数,直接调用这个函数就不是case,和case是无关的,两者并不会相互影响,可以直接调用以减少重复代码。第二个页面不同于第一个,一进入就需要网络请求,后续业务都需要依赖这个网络请求,单元测试不应该对某一个条件过度耦合,因此,需要用mock解除耦合,直接mock出网络请求得到的数据,单独验证页面对数据的响应。
总结
单元测试并不是一个能直接产生回报的工程,它的运行以及覆盖率也不能直接提升代码质量,但其带来的代码控制力能够大幅度降低大规模协同开发的风险。现在的商业App开发都是大型团队协作开发,不断会有新人加入,无论新人是刚入行的应届生还是工作多年,在代码存在一定业务耦合度的时候,修改代码就有一定风险,可能会影响之前比较隐蔽的业务逻辑,或者是丢失曾经的补丁,如果有高覆盖率的单元测试工程,就能很快定位到新增代码对现有项目的影响,与QA验收不同,这种影响是代码级的。
在本文所设计的单元测试流程中,单元测试的case和具体页面的具体业务流程以及该业务的代码逻辑紧密联系,单元测试如同技术文档一般,能够体现出一个业务逻辑运行了多少函数,需要注意什么样的条件。这是一种新人了解业务流程、对业务进行代码级别融入的好办法,看一下以前的单元测试case,就能知道与该case对应的那个页面上的那个业务逻辑会执行多少函数,以及这些函数可能出现的结果。
|
|