Android单元测试

2016/06/26

本文原创作者:Cloud Chou. 欢迎转载,请注明出处和本文链接

根据维基百科的解释,单元测试又称为模块测试。是针对程序单元来进行正确性校验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序,函数,过程等,对于面向对象编程,最小单元就是方法。

​ 通常来说,程序员每修改一次程序就会进行最少一次单元测试,在编写程序的过程中前后可能要进行多次单元测试,以证实程序满足需求。

​ 那为什么要做单元测试呢?

​ 我们首先来看看Android 程序员常见的自测方式:

​ 实现某个功能后,在手机上执行整个应用,然后在手机上操作应用,在界面上多次点击后,进入使用该功能的场景,然后测试该功能,通常只会测试功能执行的主路径。也就是说如果功能有多分支结构(if-else),那么自测时只会测试一条主路径,其它分支结构都会交给测试人员进行人工测试。

​ 那这样做有什么问题呢?

​ 如果每次实现功能时都能确保没问题,那上述流程看起来就没啥大问题了。但事实上,我们每次实现某个功能时,谁都不敢保证写完功能实现代码一定能保证它一点问题没有,谁能这么说,我也只能呵呵了。实际上,我们每次实现的功能或多或少都会存在问题,我们需要反复修改代码,来测试我们的逻辑是否正常,按照上述的流程,我们修改完代码后需要编译生成apk, 连接手机,将应用安装至手机,这几步通常就需要2分钟,然后再在手机上操作进入使用功能场景的地方,测试功能场景,并观察结果,这一般需要1分钟。所以我们每次写完代码后,需要3分钟进行验证,如果修改5次,每次定位问题并修改逻辑的时间为2分钟,那么确保该功能主逻辑没问题需要5*(2+3)=25分钟,其中60%的时间(15分钟)用于验证问题,40%的时间(10分钟)用于真正的解决问题。

​ 从这个角度看,这种自测方式非常低效,还需要程序员不断在电脑上的编程IDE和手机的应用之间切换,并且还要在手机上反复执行重复操作,对于程序员来说,其实是一件很痛苦的事情,也很容易误操作。有时候等待手机连接到电脑还要等半天,尤其是装驱动有时候要装半天,甚至adb冲突导致连接也要等非长久,有时候公司要求归还开发用的手机,换一部新的开发机,使用新的开发手机会非常不熟悉,在手机上打开调试模式都要半天,这些都是非常耽误工作效率的事情,让程序员非常痛苦。

​ 另外,这种测试通常只会测试功能执行的主流程,还有很多分支流程不会执行,交给测试人员执行时,因为测试人员通常都是做黑盒测试,所以对内部逻辑不会太了解,很容易遗漏某些分支的测试,并且这些分支的准备条件对于测试人员来说也很不好准备,所以即使知道,也会忽略某些分支的测试。而这些分支如果有问题,到了用户侧就会暴露出来了。所以这种测试没法保证功能的非主分支也能执行正常。

​ 另外,这种测试方式即使让功能的初次实现没问题了,后续迭代过程中会不断修改它的逻辑,或者它依赖的逻辑,这时候问题就出来了,因为我们修改它依赖的逻辑后,并不会再让测试同学测试这个功能。我们可能都遇到过这种场景,我们将某个Bug修改好了之后,过一段时间后,我们修改另外一个Bug,这个Bug修改好了,结果前一阵子修改的Bug又出现了。这是为什么呢?因为我们修改Bug时通常都会专注这一个Bug,修改逻辑时,只会专注于将引起这个问题的逻辑调整正确,但是将引起这个问题的逻辑调整后之后,有可能导致其它问题。比如,如果函数A调用函数C,希望它返回"Hello", 函数B调用函数C,希望它返回"Hello2", 某一天函数A希望函数C返回"HelloC"了,于是修改了函数C的逻辑,结果函数A调用函数C确实返回了"HelloC",然而函数B调用函数C也返回"HelloC"了,那么这就有问题了。而我们可能并不会注意到这个问题,就直接发了版本,到了用户侧,就有大量用户抱怨了。如果我们每次发版本时,将所有曾出现过的Bug以及所有的功能都测试一遍,那么需要非常长的时间,会严重耽误项目进度。

​ 我们再回到当初的问题,为什么做需要做单元测试,从上述几个角度已经看到通常的自测方式存在的问题,那我们看使用单元测试是如何解决这些问题的:

  1. 低效的问题

    使用JUnit的单元测试时,可以脱离手机进行代码逻辑正确性的验证,也不需要在手机上操作,执行测试用例后直接输出测试结果是否正常,如果正常就是绿颜色的执行结果,如果失败就是红颜色的执行结果,可以将验证操作的时间缩短到30s内。当然我们需要花一定的时间在编写测试用例上,不过这只是一个一次性的工作。

    使用Instrumentation Test时,不能脱离手机验证代码的正确性,但是可以节省用户在手机上操作的时间,也就省去了在手机上反复重复操作的麻烦,也能直接输出测试结果,可以很明显的根据测试结果的颜色知道代码逻辑是否正常,而通常的自测还需要思考。这种方式的验证操作需要2min20s左右。

    所以如果我们能使用JUnit的单元测试时,尽量使用这种单元测试,能极大地提升自测的效率,某些情况下不能使用JUnit的单元测试,这时候就适合使用Instrumentation Test,比如,需要本地代码的测试,就需要使用Instrumentation Test。而如果只是依赖执行本地代码的类的话,可以用Robolectric + PowerMock +Mockito 进行模拟,模拟Native类的函数,使得调用Native类的函数返回预期的值即可实现模拟,进行正常的JUnit 单元测试。Robolectric将Android的Api在PC上做了重新实现,是一套非常强大的框架,支持Android的SharedPreference,Environment, Context,甚至支持资源文件的使用(包括layout, drawable),这样我们的应用代码不用做任何改动,就可以使用Robolectric框架在PC上执行,因为Robolectric实现了Android的api,并且能在PC上运行。正是因为它,才能使得我们能脱离手机进行测试,非常强大。

  2. 非主分支无法保证正常执行的问题

    当我们测试某个功能函数时,可以为它的各个分支准备不同的测试函数,每个测试函数准备不同的测试参数,对这个功能函数进行测试,确保每个分支都能执行到。我们还可以用代码测试覆盖率工具来收集测试用例对各个分支的覆盖情况,比如jacoco工具,这样能确保各个分支都能被测试到。

  3. 需要持续回归测试的问题

    我们可以通过和持续集成服务结合来解决这个问题。我们平常解决逻辑性Bug时,可以为这个Bug编写测试用例,每次在Gitlab或者Github上如果在某个分支上更新了代码,则使得持续集成服务自动执行测试用例,这样下次修改Bug时,如果导致现有逻辑出现了问题,持续集成服务执行测试用例时,会发现错误,然后会发邮件通知提交代码的同事,说代码有问题,让其修改后提交。所有这些操作都是自动化的,可以节省大量人工测试时间,并且能提升测试效率。另外还可以将测试用例分成多组,比如SmallTest, LargeTest,MediumTest,在平常更新代码时只执行SmallTest分组的测试用例,合并代码时则执行LargeTest的测试用例,这样可以极大地节省持续回归测试的时间。

单元测试的局限性

从上面的论述看来,单元测试非常有用,但单元测试也有其局限性,整体的测试(集成测试)也是必不可少的。因为单元测试只能用于快速验证某个程序单元的实现是否有问题,而不能保证各个程序单元之间相互配合也没问题。

整体测试时也有自动化工具可以使用,比如Robotium, espresso,它们都是基于Android的Instrumentation Test实现的,也就是说如使用Robotium或者espresso编写好自动测试用例后,可以在手机上执行自动化测试操作。常规的上线前用例的测试可以使用这种自动化工具提升测试效率,不需要手动操作,如果逻辑有问题,自动化测试工具可以自动报告这种错误。

Espresso是Google写的,Google的工程师开始时也使用Robotium做自动化集成测试,在使用过程中发现存在不少问题,无法满足他们的需求,所以他们就重新实现了Espresso。Android Studio开发中的版本2.2_preview3能实现录制测试脚本,也就是说如果测试同学在手机上操作,AndroidStudio能将这些操作转换成基于espresso的java测试用例代码,然后下次测试时就可以直接执行这个测试用例了。所以如果想使用自动化的集成测试,还是建议使用Espresso。

AndroidStudio2.2 preview 3中关于录制测试脚本的网址如下所示: http://tools.android.com/tech-docs/test-recorder

Espresso相比Robotium的优点:

  1. 同步。 默认情况下,instrumentation test的逻辑并没有在UI线程上执行,而是在单独的线程里执行。如果不对测试操作和UI更新进行同步,那这些测试容易出现偶然性问题,比如说,将会因为时间的因素而随机失败。大部分测试人员都会通过sleeps/retry等机制,甚至通过复杂的线程安全代码来进行测试。这几种做法都是不理想的。Espresso可以做到在测试时无缝同步测试操作断言和应用的UI。Robotium通过sleeps/retry机制来处理这个问题,不仅不可靠,而且导致测试运行会更慢
  2. API。 Espresso有更简洁的API, 并且能支持定制。你只需通过标准的Hamcrest matchers来定位UI元素,然后在这个元素上指定某个操作或者在这个元素上进行断言即可。而如果和Robotium的API相比,测试人员需要从30多个click方法里进行选择。并且Robotium也暴露了一些危险的方法比如说getCurrentActivity以及getView,可以让你在非主线程上操作这些对象
  3. 更清楚的失败信息。当失败出现时,Espresso努力提供丰富的调试信息。并且,你能自定义你自己的Failure Handler针对Failure进行处理。

总结

为了提高代码的质量以及测试效率,针对业务逻辑要尽量使用单元测试保证各个程序单元的正确性,避免在集成时才发现问题。编写单元测试时如果能使用普通的unit test,就使用unit test,如果不行才使用instrumentation test。做普通的unit test时建议使用Robolectric+powermock+mockito。

做功能测试时,如果是单个应用的测试则建议使用Espresso做自动化测试,如果还需要和系统交互,或者需要在多个应用之间切换,则建议使用UIAutomator做自动化测试。

espresso的使用介绍: https://developer.android.com/training/testing/ui-testing/espresso-testing.html

UIAutomator的使用介绍: https://developer.android.com/training/testing/ui-testing/uiautomator-testing.html#setup

另外建议搭建持续集成服务,在向Gitlab上提交代码时自动执行单元测试(这里的单元测试是不需要连接手机就可以执行的),如果单元测试有问题则自动发邮件通知相关人员修改。通过单元测试后,在合并到master分支时,再自动进行一轮比较大的单元测试,单元测试通过后再通知相关人员做代码review,代码review的工作并不是用于保证代码没有Bug,而是用于检测代码是否符合设计理念,代码逻辑的归属类是否是正确的,有没有符合代码规范等等,做代码Review可以防范问题: 不断添加新功能导致的程序架构腐蚀,最终让程序不可控,失去了结构,象一团乱麻。

另外建议上线前的用例测试都做成自动化的集成测试用例(也是功能测试用例),避免重复劳动,测试人员将手机连接到电脑,然后执行测试用例时,测试人员可以去喝杯水了。


¥打赏5毛

取消

感谢您的支持,我会继续努力的!

扫码支持
赏个5毛,支持我把

打开支付宝扫一扫,即可进行扫码打赏哦

本篇目录