【测试左移专栏】用 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 条评论
登录 后参与评论

相关文章

来自专栏流柯技术学院

【转】gc日志分析工具

性能测试排查定位问题,分析调优过程中,会遇到要分析gc日志,人肉分析gc日志有时比较困难,相关图形化或命令行工具可以有效地帮助辅助分析。

722
来自专栏鸿的学习笔记

索引

最简单的索引策略就是:将key值的offset存入在内存,使用hash表进行管理,在搜索时,会先根据key值找到offset,进而由offset找到对应的v...

1035
来自专栏性能与架构

linux w命令查看系统负载

w命令和uptime命令相似,都可以查看系统的负载状况,但w的结果信息更丰富一些,还可以查看登录用户的状态 命令信息 $ w ? 第一行的信息和uptime的结...

39011
来自专栏北京马哥教育

Linux磁盘监控工具说明

1143
来自专栏皮振伟的专栏

[linux][intel]linux对intel c-state和p-state的支持分析

前言: 前文《[qemu][acpi]从虚拟化看ACPI》中,介绍了ACPI的大概逻辑,以及ACPI sleep的S1,S2,S3(STR),S4(STD),S...

882
来自专栏编程坑太多

悲观锁与乐观锁

955
来自专栏IT技术精选文摘

Android性能优化来龙去脉总结

一款app除了要有令人惊叹的功能和令人发指交互之外,在性能上也应该追求丝滑的要求,这样才能更好地提高用户体验。

922
来自专栏同步博客

Redis构建分布式锁

  为什么要构建锁呢?因为构建合适的锁可以在高并发下能够保持数据的一致性,即客户端在执行连贯的命令时上锁的数据不会被别的客户端的更改而发生错误。同时还能够保证命...

722
来自专栏分布式系统进阶

Kafka的消费积压监控-Burrow

893
来自专栏YG小书屋

ElasticSearch 介绍

2043

扫码关注云+社区