Android单元测试——初探

引言

这篇文章主要是总结一下我自己在学习Android单元测试过程中的收获及感悟,同时也希望可以帮助到正在学习Android单元测试的小伙伴们.由于时间及经验有限,文中可能存在错误与不足,欢迎大家指出,我会在第一时间对文章进行修改纠正.
本文主要包含以下内容:

  • 什么是单元测试
  • 为什么需要进行单元测试
  • 如何进行单元测试

什么是单元测试

首先总结一下什么是单元测试,单元测试中的单元在Android或Java中可以理解为某个类中的某一个方法,因此单元测试就是针对Android或Java中某个类中的某一个方法中的逻辑代码进行验证即测试该方法是不是可以正常工作。
还有一点就是要区分单元测试与集成测试(功能测试、UI测试),单元测试是针对单元即方法的测试,被测单元粒度要小并且具备独立性,而集成测试是测试多个单元(方法)组合成的功能模块。

为什么需要进行单元测试

  • 单元测试的测试相对于集成测试的测试成本较低
    单元测试相对于集成测试有运行时间短、投入成本低的优势即Test Pyramid理论:
    Test Pyramid

    从上图可以看出单元测试,测试速度快投入成本少
    因此我们要将大部分精力投放在单元测试中,保证单元测试的质量之后再进行集成测试与UI测试来提高测试效率
  • 提高开发效率
    开发Android App的小伙伴可能都会有这样一个体会,就是当App项目逐渐增大,运行App进行调试会花费大量时间在项目的构建、编译、打包、安装上。这个过程的持续时间与App的规模成线性相关即App项目规模越大持续时间就越久。因此随着我们的的项目逐渐增大,运行App的进行调试时,我们的调试成本也在逐渐增加。
    而单元测试正好能解决这个问题。
    举个例子:
    在登录Activity中有个checkPhoneNum方法,这个方法的功能是在点击登录按钮时,对用户输入的登录账号进行本地的合法性验证避免不必要的网络请求,如果是通过运行App来验证checkPhoneNum方法是否能够正常运行,需要经过构建、编译、打包、安装的过程,程序运行之后还需要人工操作进入登陆页面,输入账号密码,点击登录按钮,触发checkPhoneNum方法,这个过程可能需要几十秒甚至一分多钟,如果通过MVP架构将checkPhoneNum作为纯Java代码抽离出来,屏蔽对Android平台的依赖,就能将单元测试运行在JVM上,并针对checkPhoneNum方法进行测试,免去了构建、编译、打包、安装的过程,整个验证过程就在一秒之内,开发效率将大幅提升。(大致的测试流程在下个章节进行说明)
public boolean checkPhoneNum(String phoneNum){
  //判断phoneNum是否为空(实际的判断会稍微复杂一点,为了举例做了简化)       
  if(phoneNum == null || "".equals(phoneNum)){
    return false;
  }
        
  return true;
}
  • 提升项目工程代码质量
    进行单元测试前提之一就是被测单元具备可测性,以上面checkPhoneNum方法为例,如果checkPhoneNum方法中的代码直接写在登录按钮的点击事件中,而没有抽取为checkPhoneNum方法,那么对这段代码进行单元测试是会非常困难的,极端情况甚至无法测试。所以为了写出可测试的代码可以锻炼开发人员对的代码的抽象能力和加强对项目架构的把控,从而提升项目工程代码质量。
  • 快速定位Bug
    由于单元测试对被测项目中的被测单元的独立性的要求,因此在被测单元的执行结果与预期结果不一致时我们就能快速的定位到出现Bug的方法。(在下个章节中会举例说明)

如何进行单元测试

在Android中进行单元测试有很多方案,主要可以分为两类

  • 在运行在JVM上,不依赖Android环境
    如基础的 JUnit+Mockito+MVP 或比较全面的JUnit + Mockito + Dagger2 + Robolectric
    优点:测试速度快,正常情况快下都为秒级别
    缺点:存在局限性,如JUnit+Mockito+MVP是在JVM上运行的,没有Android的运行环境(没有Android相关方法的具体实现),需要对Android有依赖的单元进行依赖隔离,因此无法测试与Android相关的单元;JUnit + Mockito + Dagger2 + Robolectric虽然Robolectric模拟了Android环境,让测试代码在JVM中能够测试Android相关的单元,但是Robolectric仅支持API21及以下,并且不支持JNI库,当被测类中涉及JNI(如百度地图SDK)如果没有进行依赖隔离,测试类将会报错,无法正常运行。

  • 依赖Android环境,需要运行在模拟器或真机上
    如Android提供的Instrumentation测试框架、Espresso
    优点:测试的覆盖面大,由于运行在模拟器或真机上,因此能够测试与Android相关的单元
    缺点:运行时间长,由于行在模拟器或真机上所以会经历打包和安装的过程,导致消耗较多的时间

根据实际情况,可以灵活切换以上两种方案

如何在Android中进行单元测试

  • 首先进行相关的配置
    在Android Studio中默认情况下不需要进行配置,已经支持Instrumentation与纯JUnit,分别在androidTest与test中创建测试类,编写测试代码
Paste_Image.png

    在Eclipse中需要为被测工程添加JUnit依赖,在被测工程右键点击Properties,在窗口左侧选择Java Build Path,选中右侧Libraries,点击Add Library,选择JUnit

Paste_Image.png
Paste_Image.png

更好的做法是新建一个测试工程,将被测工程作为测试工程的依赖,再为测试工程进行如上配置,方便我们对测试代码的管理。

  • 以下对JUnit单元测试进行简单介绍,基于Instrumentation的单元测试由于是对JUnit的扩展就不过多介绍(其实是了解不够深入)

一个单元测试大概可以分为三个部分:
setup:即new 出待测试的类,为测试设置一些前提条件
执行动作:即调用被测类的被测方法,并获取返回结果
验证结果:验证获取的结果跟预期的结果是一样的

代码示例如下:

public class Calculator {
    
    /**
     * 将两个数相加
     * @param a
     * @param b
     * @return a + b
     */
    public int add(int a,int b){
        return a+b;
    }
    
    /**
     * 将两个数相减
     * @param a 被减数
     * @param b 减数
     * @return a - b
     */
    public int subtract(int a,int b){
        //将被减数与减数互换,模拟Bug
        return b - a ;
    }
}

Calculator 为被测类,Calculator 中有两个方法,也就是测试单元。add方法做加法计算、subtract方法做减法计算(subtract中将被减数与减数互换,模拟Bug)

public class JUnitTest {
    private Calculator mCalculator;
    
    @Before
    public void setUp(){
        mCalculator = new Calculator();
    }
    
    @Test
    public void testAdd(){      
        int result = mCalculator.add(1, 3);
        Assert.assertEquals(4, result); 
    }
    
    @Test 
    public void testSubtract(){
        int result  =  mCalculator.subtract(6, 4);
        Assert.assertEquals(2, result); 
    }
}

JUnitTest 为测试类,该类的创建过程可与正常类创建过程一致。
其中以@Before注解的方法中的代码对应前文中提到的三个步骤中的setUp,为以@Test注解的测试方法设置一些共有的前提条件,在这个例子中就是new出被测试类。而实际情况中可能还有相关参数与配置相关依赖或通过Mock框架进行依赖隔离等操作。
以@Test注解的方法之间是互相独立的,不存在执行上的因果关系
以testSubtract()为例

int result = mCalculator.subtract(6, 4);

对应三个步骤中的执行动作,即执行Calculator中的add方法并获得add方法的执行结果

Assert.assertEquals(2, result); 

对应三个步骤中的验证结果,Assert为JUnit提供的类,内部有一系列用于验证被测单元返回值是否与期望值一致的方法,在本例中通过Assert.assertEquals(4, result),验证mCalculator.subtract(6, 4)的执行结果result是否与预期值4相等

接下来就是运行测试类JUnitTest Android Studio中右键点击 Run ‘JUnitTest ’ 会执行JUnitTest 中所有以@Test注解的方法,并会输出验证报告
在Eclipse中需要进行配置,才能进行纯Junit的单元测试,在被测类中右键点击Run As,点击Run Configurations

Paste_Image.png

在出现的窗口中选中右侧的Classpath,默认情况下Bootstrap Entries节点下应该为Android SDK,而这里需要把Android SDK替换为JRE System Library。替换流程如下图,先将Bootstrap Entries节点下的Android SDK Remove,之后选中Bootstrap Entries节点,点击右侧的Advanced,选中Add Library,选择JRE System Library,Next 直到结束

Paste_Image.png
Paste_Image.png
Paste_Image.png

配置完成后就可以在被测类中右键点击 Run As JUnit Test,运行完成之后就会输出测试报告如下图(下图为Eclipse中的测试报告,Android Studio中类似)

Paste_Image.png

从上往下看上图,首先Failures表示有一个测试没有通过,本例中的运行时间基本可以忽略不计为(0.019s),相对于运行到真机上差别是非常大的。testSubtract测试方法没有通过,因为在被测方法中为了模拟Bug将减数与被减数互换,导致预期结果(6 - 4 = 2)expected:<2> 与实际运行结果(4 - 6 = -2)<-2>不一致.根据上图我们就能快速的将Bug定位到被测类Calculator 的subtract方法中(快速定位Bug)。

在实际的项目代码的情况会相对比较复杂,因此可以通将纯Java的逻辑代码抽离出来,具体方案有通过MVP架构将逻辑代码与Android 组件(比如:Activity)解耦,或者像上面的例子中将纯Java逻辑代码封装到类似Calculator的Utils类中,不过要尽量避免使用静态方法,这样的访问方式。(提升项目代码质量)


小结

这边只简单总结了我自己目前在学习Andorid单元测试中的感悟和收获,Andorid单元测试中其实还涉及到很多其他的技术,比如Mock的概念以及Mockito框架(隔离依赖,保证被测单元的独立性)、Dagger2依赖注入框架,配合Mockito让我们更便利的在Android中进行单元测试、MVP架构。

参考文献

推荐阅读更多精彩内容