【测试左移专栏】用 Powermock 和 Mockito 来做安卓单元测试

作者:刘洋

团队公众号:腾讯移动品质中心TMQ

一、单元测试及Android单元测试简介

惯例,先简单介绍下理论知识,懂得的可以跳过。

1、单元测试定义和特性

单测定义:

在计算机编程中,单元测试(Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。 程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。

单测特性:

截取下《单元测试的艺术》一书中的优秀的单元测试特性,牢记!

2、Android单元测试

顾名思义,是在Android系统下进行的单元测试。

业界上已经有很多工具可以支持做Android系统下的单元测试,主要分为两大类:

(1)Instrumentation

通过Android系统的Instrumentation测试框架,我们可以编写测试代码,并且打包成APK,运行在Android手机上。

优点: 逼真;

缺点: 很慢;

代表框架:Junit,Espresso。

(2)Junit / Mock

通过Junit,以及第三方测试框架,我们可以编写测试代码,生成class文件,直接运行在JVM虚拟机中。

优点: 很快,使用简单,方便;

缺点: 不够逼真,比如有些硬件相关的问题,无法通过这些测试出来;

代表框架: Junit,Robolectric, Mockito, Powermock。

Robolectric:一个单元测试框架,可以清除Android SDK(通过shadow技术),以便您可以测试驱动Android应用程序的开发,测试JVM内部运行,用例执行速度很快。

其官网地址:http://Robolectric.org/。

Espresso:一种简洁,美观,可靠的Android UI测试框架。

其API地址:https://developer.Android.com/reference/Android/support/test/Espresso/Espresso.html。

Mockito:一个针对 Java 的单元测试模拟框架,它与 EasyMock 和 jMock 很相似,都是为了简化单元测试过程中测试上下文 ( 或者称之为测试驱动函数以及桩函数 ) 的搭建而开发的工具。

其中文开发文档:http://www.devtf.cn/?p=1315。

Powermock:是在 EasyMock 以及 Mockito 基础上的扩展,通过定制类加载器等技术,实现了之前提到的所有Mockito不能模拟的功能,比如静态函数、构造函数、私有函数、Final 函数以及系统函数的模拟。

二、Google官方MVP架构

在熟悉单元测试框架前,首先需要学习了下Google官方推荐Android的MVP项目架构,好的框架单元测试也比较好开展。

其推荐的项目中MVP各层所使用的单元测试框架如下图所示:

其MVP测试架构图总结如下:

项目代码有兴趣学习的同学可以去自行下载去学习,学习这种优秀代码是最快的方式。

View层:

职责:MVP模式下,View本身该做的事情都能做了,比如UI布局,数据渲染,点击按钮交互等等。

测试方式:以正常小QA的测试思维方法,就可以来定义这一层的测试方式,测试过程中需要真机或模拟器,并做真实的操作。

测试选型:依赖于Android环境,用谷歌强大的Espresso+AndroidJunitRunner,Espresso用于模拟和验证各种各样的UI操作,代码存放于AndroidTest中。

Presenter层:

职责:这一层是拉皮条的,负责M和V层的对接,所以有较少的处理输入输出的机会,他只用来控制逻辑,去调用相应的Model和View的逻辑。

测试选型:他的职责决定了他很少去断言输入输出,测试逻辑覆盖的路径是否正确即可,因此他与Android环境无关,用Junit+Mockito测试即可,代码存放于test中。

Model层:

职责:负责数据的存取,数据可能来自于网络、数据库和内存。

数据库增删改查:需测试数据存取的准确性,依赖Android环境进行测试,因此使用AndroidJunitRunner,代码存放于AndroidTest中。

网络请求:不测试真实的网络请求,但提供了Fake供其他层调用测试。

封装的门面类:决定了数据的来源和去向是来自于本地数据库 or 网络 or 内存,此为真正对其他层暴露的Model类。此类不做数据准确性的验证,只做mock测试,验证覆盖路径。UT选型Junit+Mockito,代码存放于test中。

MVP各个模块通信方式如下:

除了MVP,还有一种MVC的方式。

MVC的全称为Model-View-Controller,即模型-视图-控制器。

Model:处理数据和业务逻辑等。

View:显示界面,展示结果等。

Controller:控制流程,处理交互。

MVC各个模块通信方式如下:

MVC和MVP区别:

在MVC模式中,View和Model可以直接交互;在MVP模式中,View和Model模块不能直接交互,View通过Presenter与Model间接交互。

在MVC中,Controller是基于行为的,可以被多个View共享,可以负责决定显示哪个View;在MVP中View和Presenter是一对一或这一对多的,并且Presenter和View是通过接口交互的。

三、单元测试环境一些基本的准备工作

1、新建一个标准的Android Studio工程

新建一个andriod Studio工程,这个就不详细说明了,网上有很多教程。

成功后src目录下就出现AndroidTest和test下目录。

2、源码和其他工程目录搬迁移植

将源码目录全部放在src/main/java下(适合老业务改造)。

如果源码目录指定不对,需要修改build.Gradle的sourceSets配置。

3、增加工具框架依赖

在dependencies下增加工具框架的引用。

注:如果用到什么框架就将框架引用进来即可,但有些工具主要版本号的相互搭配,不匹配可能会出现错误。

网上有一个PowerMock对Mockito的版本对应关系:

作者使用的是下面红色的组合,请根据实际情况匹配。

4、增加Jacoco覆盖率

增加Jacoco的插件:

指定版本号和报告目录:

指定源码目录。 自定义Jacoco报告规则task:

上面一切准备完毕后,配置好代码,Gradle就可以正常同步加载了。

如果你的Android Studio的Gradle Sync同步成功,那么恭喜你单测环境基本OK了,依赖库基本也已经下载完毕,下面可以愉快的开始着手代码编写了。

可能有的公司需要网络代理,那这个需要根据具体情况在Gradle中配置了。

四、编写AndroidTest下的单测用例

UI层的单元测试只简单介绍一下,作者实际编写单元测试的时候,UI部分的单元测试用例也是放在了test目录下一起写的(PowerMock模拟的),运行不需要手机或模拟器,执行速度比较快。

虽然没有在实际项目中大量使用,但也将当初的尝试简单介绍一下,供参考。

UI的Instrumentation用例可以选取Espresso。

在AndroidTest目录下新建一个测试类。

比如我们测试一个这样的单测用例:测试更新页的点击更新所有,用户页面会弹出一个toast确认的弹框。

用例编写如下:

手机连上电脑,选中用例鼠标右键run就可以运行看结果了。

五、编写test下的单元测试用例

首先介绍下单测工具框架选取的过程。

1、选取合适的测试框架

作者开始在业务中尝试使用Robolectric测试框架,初心主要在于他的特性:

Robolectric Test-Drive Your Android Code Running tests on an Android emulator or device is slow! Building, deploying, and launching the app often takes a minute or more. That's no way to do TDD. There must be a better way. Wouldn't it be nice to run your Android tests directly from inside your IDE? Perhaps you've tried, and been thwarted by the dreaded java.lang.RuntimeException: Stub!?

它不需要Run你的模拟器,直接在jvm上运行你的测试代码,能在短时间之内快速验证,通过体验之后,它确实非常高效,编写测试代码反而加速了开发效率。

另外被它强大的Shadows方式所吸引,可以完全实现自定义方式。

但在实际使用的过程中遇到了不少的坑,比如:

Robolectric版本和SDK版本强依赖。

compileSdkVersion 23的不能使用Robolectric:3.0的版本,只能使用Robolectric:3.2.2以上的。为什么会有这种强依赖,是因为Robolectric会shadow大部分Android的代码,会有很多shadow的类,也就会随sdk版本的变化而变化。

Robolectric首次启动下载maven相关的依赖失败。

即使我们在开发网下设置了代理,开通外网权限,首次启动还会去下载相关依赖,结果是下载失败,这个是由于Robolectric本身代码里的逻辑,我们不能通过网络代理的方式解决。

唯一的办法只能一个一个手工的下载后丢到你的.m2\repository\org\Robolectric目录下,让Robolectric找到其所依赖的jar包,不需要在去下载,如下:

如果在build.Gradle重新指定Robolectric的版本,那么这些需要的版本还要手工下载一遍。

Robolectric运行报TinkerRuntimeException: Tinker Exception:onCreate method not found 业务使用了Tinker多包加载架构,运行出现上面的异常。

解决方法: RealAstApp里面人为增加oncreate方法 @Override public void onCreate() { } 这样修改代码其实是有点犯忌的,但只改这一处还勉强可以接受,下面的就不能接受了。

Robolectric运行在自定义的控件时有时会出现xml解析异常。 跟踪解决了几个,发现要修改的地方比较多,这里省略一万字的修改记录。 除了改动点比较多,也可能后续会出现更多的潜在错误。 违背上面的单元测试特性之运行稳定,衡量再三,还是决定放弃Robolectric了,另寻它径。

这里也声明下,Robolectric工具还是很优秀的,它的解决思路很清晰,所有调用到Android相关的都会转移到其shadow类,这样就可以完全脱离Android的限制,只是由于业务的特殊性才暂时不用。

于是又开始研究Espresso,见上面的(编写AndroidTest单元测试用例)。 使用过程中总体感觉Espresso功能比较强大,只要合理的使用其提供的api和matches规则,常用的UI逻辑基本都可以模拟,但唯一不爽的就是每次都要连接手机或者模拟器才能运行,Run的过程中,首先会打包,部署到手机上,然后再开始一个一个运行测试用例,好处是手机上的表现很直观,但这样调试和运行速度是真心的慢。

违背上面的单元测试特性之运行速度快,建议放弃。

尝试使用Junit、Mockito和Powermock来编写MVP三层的单元测试用例,在经过一阵探索后,MVP三层的逻辑基本都可以通过Mockito和Powermock来模拟出来,运行起来关键是速度快,速度快,速度快,好的地方说三遍。

上面的单元测试特性也基本都能满足,最终决定使用Junit、Mockito和Powermock这个框架组合来进行我们的单元测试用例设计和编写。

2、选取被测模块和熟悉被测模块的代码逻辑

在单元测试前要对被测模块有个大致的代码逻辑熟悉,对代码的深入可以边写边熟悉。

3、PowerMock知识点掌握

单测用例编写过程中,熟练程度一部分完全取决于对单测工具框架的了解程度,这块没捷径可走,必须要掌握清楚明白,简单列一下其知识点,具体还是需要自己去搜索资料掌握的。

(1)PowerMock注解@RunWith与@PrepareForTest的使用;

(2)测试或模拟static方法;

(3)测试或模拟返回void的静态方法;

(4)PowerMockito.doNothing与PowerMockito.doThrow的使用;

(5)如何验证方法调用;

(6)如何验证调用次数的方法;

(7)测试或模拟final类或方法;

(8)测试或模拟构造方法;

(9)如何做参数匹配;

(10)Answer接口的使用;

(11)如何使用spy进行部分模拟;

(12)如何测试或模拟私有方法;

(13)@Before和@Test的作用;

(14)如何给私有的字段赋值;

(15)如何模拟异常。

4、设计单元测试用例

需要写单测case列表。

在我们的项目中,单元测试对象建议和类相对应,这样的单元测试结果比较直观。单元测试分析被测类的业务逻辑,这里的逻辑不仅仅包括界面元素的展示以及控件组件的行为,还包括代码的处理逻辑。然后可以创建单元测试case列表,列表用于纪录项目中单元测试的范围,便于单元测试的管理以及新人了解业务流程,列表中记录单元测试对象的页面,对象中的case逻辑以及名称等,测试或开发工程师可以根据这个列表开始写单元测试代码。

用覆盖率来校验单测用例是否完备。

单元测试是工程师代码级别的质量保证工程,上述流程并不能完全覆盖重要的业务逻辑以及边界条件,因此,需要写完后,看覆盖率,找出单元测试中没有覆盖到的函数分支条件等,然后继续补充单元测试case列表,并在单元测试工程代码中补上case。

直到被测类所有逻辑的重要分支、边界条件都被覆盖,才认为该类的单元测试结束。

另外觉得复用或通用的逻辑建议做成工具类,直接复用。

整理了一个case的单测流程图,供参考:

5、公共的可复用的抽离出成工具类

将一些常用的场景抽象出工具mock类,如BundleMock、HandlerMock、IntentMock、MainThreadHandler、ParcelMock等等,这样提供给单测直接调用即可,不用在重复造车轮。

6、几种场景的单元测试用例案例

单元测试用例设计,格式可以自己灵活去定义,另外也可以在代码中已Javadoc的方式添加单元测试用例内容,输入、输出、断言几点明确就可以了。 我们把一部分项目常用的场景通过mock实现后,剩下的基本都是工作量的问题了。

目前业务代码逻辑场景的模拟做了如下:

(1)请求的模拟及回调onRequestSuccessed和onRequestFailed的模拟;

(2)页面inflate加载场景模拟;

(3)页面findViewById加载场景模拟;

(4)控件onclick场景模拟;

(5)数据回调场景模拟;

(6)主线程handler场景模拟;

(7)序列化的模拟;

(8)intent的模拟;

(9)其他等等。

这部分的模拟代码就不贴上来了,有点多,需要可以线下交流。

7、单测类的编写经验

(1)mock对象可以被整个类的测试方法共用的,mock时统一放到@Before里init;

(2)mock对象仅供单个单测用例使用的,mock时可以直接放到单测用例里;

(3)能抽象出来的mock对象,建议做成工具类调用;

(4)单测用例一定要有断言,且断言准确,这样才能保证单测用例的有效性;

(5)不要怕麻烦,开始都会感觉很难,写多了熟练了就好了。

8、debug调试

执行时候如何出现一堆黄色的PASSED,心里当然感觉爽了。

但在单元测试编写运行中难免会出现各种异常错误,mock时出现空指针的场景会比较多,这时候我们就需要用debug调试方式。

然后设置断点,通过F8逐步跟踪下去吧,找出单测用例的编写的问题所在。

9、生成覆盖率报告

在Android Studio的Terminal中输入Gradlew JacocoTestReport后,单元测试开始运行,无错误结束后就会在指定的报告生成目录下看到覆盖率结果了。

通过覆盖率结果,查看到单测case覆盖情况,根据情况补充或修改单测用例,加大覆盖率结果的提升,单测是有望达到100%覆盖的。

单测过程中可能会出现某些类的覆盖率结果为0的,但实际上应该有覆盖率的,这可能是由于一些页面单测场景下被测类在@PrepareForTest中声明了,导致这些类的覆盖率为0。以前作者也介绍过Jacoco的原理,其是修改class字节码文件插桩的,但再经过PrepareForTest这种指定后,PowerMock也会修改class的字节码,这样就把Jacoco的插桩冲掉了,导致覆盖率为0,这部分我们可以通过自己写脚本的方式来算覆盖率,然后在和Jacoco的覆盖率相叠加算出总的覆盖率。

六、做单测的意义

现在各个项目的代码量都比较庞大,全部进行单测覆盖,工作量消耗是非常巨大的。

并且产出和收益也不一定成正比例。

其实我们做单测和做系统测试的出发点都是一样的,提升项目的总体质量。

两点实施方式:

1、对于开发久,稳定的功能,单测的出发点为系统功能测试的互补。

单测的着重点在功能测试难覆盖的地方,通过单测发现功能测试难发现的问题及代码潜在的问题。

2、对应刚开发,新功能,如果有时间和人力的话,可以考虑单测全覆盖。

尽量在开发编码时并行实施,或者推动开发自己写单测。

最后有一个话题有机会大家可以一起讨论下:

单测的投入和产出如何来平衡?

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏中国Android研究院

番外篇-Flutter初识三问

在Android中,您可以通过直接对view进行改变来更新视图。然而,在Flutter中Widget是不可变的,不会直接更新,而必须使用Widget的状态。

993
来自专栏前端学习心得

浅聊常见浏览器的兼容性问题

1138
来自专栏Golang语言社区

开发者必备的12个JavaScript库

现在 web 设计是最有趣的了,做好 web 设计不仅要熟练使用 Javascript,css 和 html 等,还要有自己的创意设计。为了方便大家发挥自己的创...

2779
来自专栏君赏技术博客

怎么让继承的类直接使用XIB的布局试图

最近做的一个小工具,一键替换key,就是为了解放双手,不然每次运行测试和正式的版本都要手动的替换key。

792
来自专栏葡萄城控件技术团队

如何在施工物料管理Web系统中处理大量数据并显示

最近在开发施工物料管理系统,其中涉及大量的物料信息需要管理和汇总,数据量非常庞大。之前尝试自己通过将原始数据,加工处理建模,在后台代码中通过分组、转置再显示到 ...

20110
来自专栏Aloys的开发之路

Python第三方常用工具、库、框架等

       Python ImagingLibrary(PIL):它提供强大的图形处理的能力,并提供广泛的图形文件格式支持,该库能进行图形格式的转换、打印和显...

34310
来自专栏web前端教室

面试时对方问你,“xxx需求你是怎么做的”?你可以这样回答

如何去分析一个需求?这两天在给零基础课的同学们讲课的时候,正好讲到了,产品详情页的图片展示这里,ui是这样的, ? 跟京东那个的ui交互操作是一样的,我跟他们讲...

1735
来自专栏WeTest质量开放平台团队的专栏

[分享干货晒技术]Unity 手游内存优化分享

Mono下的foreach使用需谨慎。频繁调用容易触及堆上限,导致GC过早触发,出现卡顿现象。

782
来自专栏小樱的经验随笔

洛谷 P1598 垂直柱状图【字符串+模拟】

P1598 垂直柱状图 题目描述 写一个程序从输入文件中去读取四行大写字母(全都是大写的,每行不超过72个字符),然后用柱状图输出每个字符在输入文件中出现的次数...

2825
来自专栏程序员的SOD蜜

“老坛泡新菜”:SOD MVVM框架,让WinForms焕发新春

火热的MVVM框架 最近几年最热门的技术之一就是前端技术了,各种前端框架,前端标准和前端设计风格层出不穷,而在众多前端框架中具有MVC,MVVM功能的框架成为耀...

3086

扫码关注云+社区