本文不会用各种高大上的理由试图去说服你写
单元测试,只是描述笔者在单元测试这条路上一路走来的思考和简单的示例,如果顺便能让你觉得单元测试其实也没那么遥远、回头也在实际项目中尝试一下,估计就是本文最大的收获了。
一、提起单元测试,你对它的映像是什么?
大部分同学,可能都不了解单元测试,在实际项目中觉得这根本就是在浪费时间:我撸代码快的飞起,撸完交给 QA 测试就好,没有必要、也不用做单元测试,真的很多余,况且项目中一向都是这么干没发现啥问题。
另一部分同学,多少听说过或者稍微了解过单元测试,认为单元测试或许很重要但其实不知道重要在什么地方,当然也不知道怎么去写、到底哪些部分要写单元测试。
最后,差点漏了已经把单元测试应用在实际项目中且驾轻就熟的同学,为你们鼓掌!嗯,这篇
文章不太适合你们。
笔者就是经历过从不了解到了解再到应用的过程。
刚开始工作那一两年干前端,因为不是科班出身,根本没怎么听过单元测试,后来开源项目见多了(基本上是国外的),才知道还有单元测试这个东西,但觉得离自己很遥远,团队也没有要求写。
随着对 TDD 等概念的了解,特别是转型移动开发后,官方开发者网站上都有单独的篇幅重点介绍怎么测试(当然也包括了单元测试),越发觉得它或许很重要,但并没有深入的理解,当然也不知道我们到底为什么要写单元测试,看上去还增加了额外的工作量。
当项目越做越大,开发者、代码量也越来越多,慢慢就会暴露出一些问题,也是笔者在实际项目中遇到的痛点(若站在团队的高度考虑问题,你对笔者的这些痛点会更有同感)。
第一个痛点是人,为什么这么说呢?因为人是不靠谱的,如果你花一下午一边喝咖啡一边写完了一个功能模块,一跑 0
bug,包括各种边界和异常你都全部考虑到了,那你是大神。但飞机也有失误的时候,更何况大神呢,大神也有状态不好的时候,所以,是人写出来的代码他都会有 bug,我们的目的是如何去减少它,保证代码质量。我们现在都是寄希望于 QA,整个周期太长,想找个 QA/开发坐在你旁边结对编程吧,代价太高别人还不愿意!而单元测试恰恰可以帮助我们做到这点,它就像是一个趟在硬盘里的 QA 机器人,随叫随到,实时提供质量保障服务,想想都有点高大上!
第二个痛点是编译,我想
Android 开发深有体会,编译时间太长了,修改一个 bug 后运行,打完水回来还在 building。如果只是小问题,这么来来回回太浪费时间了,时间就是生命啊!
第三个痛点是边界,有些边界在真机测试中是很难构造的,借助单元测试可以突破条件的限制,做为一个有经验且略带强迫症的开发我们理应不漏掉任何边界。
第四个痛点是重构,随着代码质量意识逐渐提升撸代码功力也在不断加强,业务也在不断发展变化,其实你会发现不少可以重构的模块,想对某段代码下手,又因为不知道影响边界而放弃。好不容易改了吧,其实心里是没有底的,都要 QA 去测试,但 QA 做的是
黑盒测试,有一些异常情况我们开发很容易想出来但是 QA 很难测到,无形中增加了心理负担。
为了解决以上痛点,笔者开始了解 Android 单元测试,并运用在实际项目中。
二、什么是 Android 单元测试?
单元测试就是针对程序最小单元进行正确性校验的工作[1]。以 OOP 为例,OOP 中最小单元就是方法,单元测试就是对方法的测试。通俗点讲就是,我写了一堆方法,需要自己保证每个方法的输入输出是正确的。
来看看 Android 官方的测试金字塔[2]:
App 测试金字塔
它把 Android App 所涉及到的测试分成了三类,从下往上分别是单元测试,集成测试和 UI 测试。
单元测试是可以在本地快速执行的,组件可以通过 mock 生成。其特点是快!
集成测试只能跑在虚拟机或者真机,集成了多个系统组件,如无法 mock 的组件—相机调用,可用于单个页面的逻辑正确性测试。其特点是慢!
UI 测试就是模仿用户真实行为的测试,涉及完整业务流程,这个我们最为熟悉,每次提测 QA 都在手动或者自动做这部分工作。
不同的测试类型,侧重面不同,性价比也不同,官方推荐的测试比重是 7:2:1[2],也就是说单元测试性价比最高应该占整个测试的 70%。
单元测试是测试每个方法的正确性,只要保证每个方法都没有问题,那么由这些方法组成的模块也不会有太大问题(出问题要么是边界没考虑全要么是流程有问题),一定程度上起到减少 bug 率的作用,实际项目中已经不记得多少 QA 提的 bug 都是人为疏忽导致得了,通过单元测试都可以轻易避免啊喂。
Bug 少了,人也精神了,简直不要太爽!
三、我们在写 Android 单元测试时到底在测什么?
首先,不知道你是否也有这样的疑惑:单元测试都是针对纯 Java 的,Android 开发和系统组件有着千丝万缕的联系,所以 Android 项目中能写单元测试的类不多。笔者过去一段时间都是持这种观点的,大部分 Android 开发同学也不乏这么想的。
其实不然,借助第三方库如 Robolectric 同样可以像纯 Java 类一样去测试那些依赖系统组件的业务类。
之所以有“依赖系统组件的类不能单元测试”的误解,官方也有一定的责任,Android Studio 创建的项目默认只有 JUnit4 单元测试示例,要测试系统组件依赖的类,官方的示例是通过 AndroidTest,这是要跑在真机或者虚拟机上的,不是真正意义上的单元测试。
也就是说 Android 开发中所有的类都可以被单元测试。
其次,我们测试的目的有这几个方面,统称为 Right-BICEP[3] 原则:
Right – Are the results right? 结果是否正确?
B – are all the boundary conditions correct? 所有边界条件都是正确的么?
I – can you check the inverse relationships? 能否检查一下反向关联?
C – can you cross-check results using other means? 能够使用其他手段交叉检查一下结果?
E – can you force error conditions to happen? 是否可以强制错误条件产生?
P – are performance characteristics within bounds? 是否满足性能要求?
三、Android 单元测试要怎么做?
要明确一点是,单元测试是一门需要学习的
技术,无论单元测试、集成测试还是 UI 测试,他们都分别有自己的技术栈。如果你觉得单元测试需要花很多时间或者无从下手,或许是因为你对这门技术掌握得还不够多不够熟练,再者可能项目也没有很好的测试框架方便我们去写测试代码。
同时先介绍两个概念:Mock,这是 Android 单元测试最重要的概念,Mock 是指模拟出一个虚拟对象,替换我们原先依赖的真实对象,避免类之间相互影响。另外一个重要概念是 Shadow,是指在 Android SDK 类基础上封装一层影子类(如 Activity 和ShadowActivity、TextView 和 ShadowTextView 等),这些影子类,丰富了系统类的行为,提供测试接口。
关于测试理论和技术已经有很多成熟的资料,笔者也写不出什么新的花样,也不是本文的目的所在,这里不在做过多讲述,直接给出结论。
笔者最终选择的 Android 单元测试技术栈是 JUnit4、Mokito、 Robolectric、JaCoCo、GitLab。
JUnit4 是纯 Java 单元测试框架,在创建项目时,Android Studio 已经搭建好,我们直接使用即可。
Mokito 是用来 Mock 依赖的类或者接口,对那些不容易构建的对象用一个虚拟对象来代替。
Robolectric 则在 JVM 中实现了 Android SDK 运行的环境,让我们无需运行虚拟机/真机就可以跑单元测试。
JaCoCo、GitLab 用来搭建单元测试报告平台,可实现定期运行、自动采集、错误报警,提供覆盖率、通过率数据查看,后面笔者会专门写一篇文章进行介绍。
我们来看看 JUnit4、Mokito、 Robolectric 在项目中如何使用,在 module 下的 build.gradle 文件中进行配置:
// 单元测试
JUnit(由 IDE 自动创建)
testImplementation ‘junit:junit:4.12’
// 单元测试 Mokito
testImplementation “org.mockito:mockito-core:2.18.3”
// 单元测试 Robolectric
testImplementation “org.robolectric:robolectric:3.8”
就这么几行代码,Android 单元测试基础环境搭建完成,可以说很简单了。
我们用简单的例子说明它们的用法。
假设我们有 Math 类,提供 add(int a, int b) 方法:
待测类
想给 add 方法写
测试用例,Android Studio 中的快捷方式是,对这个类进行右键 -> Go to -> Test。
创建测试方法(1)
点击 Create New Test。
创建测试方法(2)
在弹出框中,勾选对应方法,然后点击 OK 按钮,其他保持不变。
创建测试方法(3)
在目录选择弹框中,选择 ../test/… 目录,这个才是单元测试的目录,目录的选择决定了是单元测试还是集成测试。然后点击 OK。
创建测试方法(4)
单元测试类/方法就创建好了。
测试类
接下来我们写测试代码,其中 Assert 断言和 @Test 注解就用到了 JUnit。
测试代码
点击测试方法左侧的小三角(对方法右键也行),选择 Run xxx,很快就有结果。
运行测试用例
测试用例运行结果
上述例子说明了如何创建测试类/方法,以及简单的 JUnit 用法,我们来改造一下 Math#add 方法:参数改成 String 类型,在相加之前先调用 Validator 对象判断所传入的参数是否为数字,而 Validator 通过 init 方法传进来,目的是为了可测试(有时为了可测试,我们要调整编码思维,本例中的处理方式只是为了说明问题,并非最优方案,也并非通用方案)。
改造后的 add 方法
这种情况 add 方法依赖了一个外部类 Validator,我们要测试其正确性就要排除依赖的影响,这样做的目的是错误隔离,也就是 Validator 自身的 bug 不会影响到 add 方法,Validator 由其对应的测试用例去保证。
隔离的好处是如果 add 测试用例运行失败,那么就能确定问题出在 add 方法中,和 Validator 没什么关系。
如何排除依赖类的影响呢?这时候就要请出 Mockito 库了。我们来看看测试用例怎么写。
通过 Mock 隔离依赖
具体直接看注释,这就是 Mockito 使用的例子。
另外一个例子是,我们有一个 Activity,上面有一个输入框、一个按钮、一个 TextView,输入框输入人名 Peter, TextView 输出 Hello, Peter! 截图如下。
界面示例
因为依赖系统组件,我们用 Robolectric 写测试用例。
Robolectric 单元测试
同样请直接看注释。
以上就是 JUnit、Mockito、Robolectric 组合使用的示例,为了便于理解(引人入坑),举的例子都比较简单,实际应用中其实可以玩出很多花样。