团队公众号:腾讯移动品质中心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内部运行,用例执行速度很快。

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

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

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

在熟悉单元测试框架前,首先需要学习了下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之间是一对一或一对多的关系,并且它们是通过接口进行交互的。

接下来是关于单元测试环境的一些基本准备工作:

1. 新建一个标准的Android Studio工程。新建一个Android 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:在Android模拟器或设备上运行测试速度慢!构建、部署和启动应用程序通常需要一分钟或更长时间。这种方式无法进行TDD(测试驱动开发)。有没有更好的方法呢?直接从IDE内部运行你的Android测试岂不更好?也许你已经尝试过,但被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本身代码里的逻辑,我们不能通过网络代理的方式解决。

在Android开发中,我们经常需要进行单元测试。为了方便地管理依赖库,我们通常会将它们放在一个特定的目录下。然而,有时候我们需要手动下载这些依赖库并将它们放到正确的位置。这是因为当我们使用build.Gradle重新指定Robolectric的版本时,我们需要手动下载所需的版本。

在某些情况下,使用Robolectric运行应用程序时可能会遇到TinkerRuntimeException异常,提示onCreate方法未找到。这是因为应用程序使用了Tinker多包加载架构。为了解决这个问题,我们需要在RealAstApp类中人为地增加一个onCreate方法,如下所示:

```java

@Override

public void onCreate() {

}

```

虽然这种修改代码的方法可能不太合适,但只要只修改这一处,还可以接受。然而,如果要修改更多的部分,就不再可接受了。此外,在使用Robolectric运行自定义控件时,有时会出现xml解析异常。为了解决这个问题,我们需要进行大量的修改。由于修改点较多,后续可能会出现更多的潜在错误。考虑到单元测试的稳定性,我们最终决定放弃Robolectric,寻找其他解决方案。

在这里,我们也要声明一下,Robolectric工具确实非常优秀。它的解决思路非常清晰,所有与Android相关的调用都会转移到其shadow类中,从而完全脱离Android的限制。然而,由于业务的特殊性,我们暂时无法使用它。

于是,我们开始研究Espresso。总体来说,Espresso的功能非常强大。只要合理地使用其提供的API和matches规则,常用的UI逻辑基本都可以模拟。但是,唯一让人不满意的地方是每次都需要连接手机或模拟器才能运行。在运行过程中,首先需要打包并部署到手机上,然后再逐个运行测试用例。虽然这样可以在手机上直观地查看表现,但调试和运行速度确实很慢。因此,我们决定放弃Espresso。

接下来,我们尝试使用Junit、Mockito和Powermock来编写MVP三层的单元测试用例。经过一段时间的探索后,我们发现MVP三层的逻辑基本上都可以通过Mockito和Powermock来模拟出来。关键在于速度快,速度快,速度快!

单元测试框架的选择和准备为了进行单元测试,我们选择了Junit、Mockito和Powermock这个组合框架。在开始编写测试用例之前,我们需要对被测模块有一定的了解,包括代码的逻辑结构。此外,我们还需要熟练掌握PowerMock的各种知识点,这将有助于我们更好地编写测试用例。

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. 如何模拟异常。

设计单元测试用例

在开始编写测试用例之前,我们需要设计一个单元测试case列表,用于记录项目中单元测试的范围和对象。建议将单元测试对象与其对应的类相对应,以便更直观地查看测试结果。单元测试需要分析被测类的业务逻辑,包括界面元素展示、控件组件行为以及代码处理逻辑等。创建好单元测试case列表后,可以根据这个列表开始编写单元测试代码。

使用覆盖率校验单测用例完备性

在编写完单元测试用例后,我们需要使用覆盖率来检查是否已经覆盖了所有重要的业务逻辑和边界条件。如果发现有未覆盖到的函数分支条件等,就需要继续补充单元测试case列表,并在单元测试工程代码中补上相应的case。只有当被测类的所有重要分支和边界条件都被覆盖时,才认为该类的单元测试已经完成。

建议将复用或通用的逻辑封装成工具类,以便于直接复用。最后,我们整理了一个case的单测流程图,供大家参考。

以下是重构后的内容:

1. 抽象常用场景的工具mock类,如BundleMock、HandlerMock、IntentMock、MainThreadHandler、ParcelMock等,以便在单元测试中直接调用,避免重复造轮子。

2. 设计单元测试用例格式,可以在代码中以Javadoc的方式添加单元测试用例内容,包括输入、输出和断言等要点。对于部分常用场景,可以通过模拟实现,减少工作量。

3. 模拟业务代码逻辑场景,包括请求模拟及回调、页面inflate加载、页面findViewById加载、控件onclick、数据回调、主线程handler、序列化、intent等。具体的模拟代码较多,可线下交流。

4. 在单测类中编写mock对象时,可以将共用的mock对象放在@Before注解的方法中初始化;仅供单个单测用例使用的mock对象可以直接放到单测用例中;建议将能抽象出来的mock对象做成工具类调用;单测用例需要有断言且准确,以保证有效性;不要怕麻烦,多写熟悉后就会觉得简单。

5. 在执行单元测试时,如果出现一堆黄色的PASSED,心理上会感到很爽。但在编写运行过程中,难免会出现各种异常错误,如mock时出现空指针的情况较多。这时可以使用debug调试方式,设置断点并逐步跟踪,找出单测用例编写的问题所在。

6. 通过在Android Studio的Terminal中输入Gradlew JacocoTestReport命令,启动单元测试并生成覆盖率报告。无错误结束后,会在指定的报告生成目录下看到覆盖率结果。根据覆盖率结果查看单测case覆盖情况,根据需要补充或修改单测用例,提高覆盖率,有望达到100%覆盖。

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

做单测的意义在于提升项目的总体质量。对于开发久、稳定的功能,单测出发点为系统功能测试的互补。单测着重点在功能测试难覆盖的地方,通过单测发现功能测试难发现的问题及代码潜在的问题。对应刚开发、新功能,如果有时间和人力的话,可以考虑单测全覆盖。尽量在开发编码时并行实施或推动开发自己写单测。

最后有一个话题有机会大家可以一起讨论下:单测的投入和产出如何来平衡?