单元测试是指对一个工作单元的测试。一个工作单元可以是一行代码、一个方法、一个甚至几个类。本文主要聊聊单元测试是什么,为什么要写单元测试,简单介绍一下 Android 中的单元测试。

单元测试是什么

测试作为最常见的改善质量的活动,而测试中的单元测试作为提升代码质量、理解代码的最佳手段,并不是一个新概念,早在使用 SmallTalk 编程时代就开始出现了。维基百科中对单元测试的定义是这样的:

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

该定义中提及到的模块也就是一个单元。对于被测试程序来说,指调用一个方法并得到一个可以验证的输出过程中所涉及的所有代码统称为一个单元。单元测试不一定要尽可能的小,比如我们不一定要对一个类的每个方法都进行测试,也可以对多个方法组合成的小单元进行测试,这样使得测试代码更容易维护。如果创建的单元太小,则有可能会准备一堆测试所需的前提条件,也有可能造成过度测试。但是单元测试一定要从完整的系统中隔离出来进行测试。

单元测试的目的隔离其它的单元(或者部件)而验证当前单元的正确性。从TDD的角度来讲,开发人员必须在开发代码之前就要写好测试用例,并根据测试用例完善代码。从代码修改或者重构的角度来讲,开发每次修改代码都必须运行一次测试用例,以验证本次修改的准确性。

为什么写单元测试

单元测试的目的是保证每一个被测试单元的运行准确,没一个被测试单元的运行准确,是保证整个系统运行正常的基础。人非神,不能保证永远不犯错,正如没有人能保证自己写的代码永远不会出错一样。只有当你的代码经过了充分的测试才能保证它不会把错误带到用户手中。

当自己千幸万苦终于写出了一个 App,并经过漫长时间的 debug 模式,终于这个 App 可以在手机上打开了,但是下一刻,有可能一个无情的错误弹框会出现在眼前。相信每个工程狮都遇到过类似的情况,可能是因为在代码里面没有考虑 null?或者没有考虑极端情况?等等。

这些基础的错误完全可以在开发初期就可以排除并解决掉,这也是初期做好单元测试的重要性。测试完全可以跟着开发走,或者测试先于开发。
单元测试还具有以下优点;

  • 速度快 - 主要表现在三个方面:
    • 从 Android 的角度来讲,不需要编译成字节码文件,因此编译时间短;
    • 从编写角度讲,编写一条单元测试用例的时间相对较少;
    • 从执行速度来看,单条测试用例的执行时间基本上是毫秒级的;
  • 不依赖设备 - 只需要在 JVM 上运行就可以,不需要其他的设备 (例如 Android 设备化测试必须要依赖手机);
  • 能对缺陷进行快速反馈,减少开发、维护时间、精力等;
  • 单元测试可以对重构提供很好的保障;
  • 更少的 Bug,或者更快的发现 Bug,可以在开发初期就可以解决很多Bug;

单元测试的重要性不言而喻,尤其是在开发或者重构的过程中起到了举足轻重的作用。Google 官方文档中对 Android 测试中单元测试、集成测试、UI(设备化)测试描述的金字塔如下:

test-pyramid

由此可见单元测试处于金字塔最下一层,也是整个测试的基石。Google 对单元测试、集成测试、UI 测试的测试用例所占比例建议为70%、20%、10%,由此也可以看出单元测试的重要性。

Android 中的单元测试

Android 中的单元测试可以分为Instrumented unit testLocal unit test,即设备化的单元测试和本地化的单元测试。

Instrumented unit test

有些测试用例在运行的时候使用到了 Android Framework 中的一些东西,这类测试用例在测试的时候需要借助设备运行,除非把和 Android Framework 相关的使用都 Mock 掉,这类测试即设备化单元测试,该类测试默认在<module-name>/src/androidTest/java目录下;

Instrumented unit test 运行在 AndroidJunitRunner之下,AndroidJunitRunner 是测试支持库下(Android Support Library)内包含的一个 Junit 运行器,可以使用它在 Android 设备上运行 JUnit 用例。测试用例可以是 JUnit3 或者 JUnit4,但是不要进行混用,不然会出现意想不到的错误。如果测试用例需要使用 AndroidJunitRunner 来运行,则测试类要添加 @RunWith(AndroidJUnit4.class)注释。使用 AndroidJunitRunner 需要在 build.gradle文件中指定 testInstrumentationRunner

android {
defaultConfig {
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
}

Local unit test

Local unit test 在运行的时候不需要借助设备,不会使用 Android Framework 或者是把 Framework 相关的代码进行 Mock 处理,使之可以在 JVM上运行,这类测试用例默认在<module-name>/src/test/java目录下;

在 Android Local Unit Test 中,为了使测试用例脱离 Android Framework,常用到的方法是使用MockPowerMock 或者 Robolectric 框架,如果需要使用三个框架,需要引入必须的依赖文件,这个后面文章会谈到,这里只是对这几个框架做个简单的介绍:

  • Mockito - Mock 意为模拟,是指对一个类、接口等进行模拟 ,并且利用插桩的方法可以指定当调用到类、接口的方法时可以返回特定值、做特定的操作,常用来在团队开发的过程中对其它没开发完的类、接口进行mock,假设该类、接口正常工作的情况下来测试已经开发完成的代码是否正常工作.

    但是 Mock 框架有个限制就是只能对公有的方法进行插桩,不会造成很大的代码侵入;

  • PowerMock - PowerMock 和 Mock 相似,其实也有很多代码是继承自 Mock 框架,但是比 Mock 框架更强大,它可以做一切 Mock 框架可以做的事,还可以对静态、final、私有的方法、变量等进行操作,如果老板要求你代码覆盖率达到100%,那么 PowerMock 是个神器;

  • Robolectric - Robolectric 框架对Android Framwork 进行重新的实现,使之可以脱离设备运行,该框架基本上对 adnroid.jar 包内的每一个类Xxx都做了一个实现ShadowXxx.

    Robolectric 框架可以对 Acticity、BroadcastReceiver、ContentProvider、Service四大组件进行测试,比如可以测试Activity 生命周期、ContentProvider 数据是否存储成功等;除此之外还可以对 Dialog、Toast、资源等进行测试;

最好的解决方案即是 PowerMock + Robolectric,用 Robolectric 框架测试 Android 组件,辅以 PowerMock 测试框架来测试逻辑代码或者其它,以达到很好的测试效果。