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架构。

参考文献

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,832评论 25 707
  • 之前一直忽略单元测试,遇到问题的时候 (修改-运行-等待-操作-验证) 或者 (修改-debug-操作-验证),每...
    joxHero阅读 2,223评论 3 42
  • @Author:彭海波 前言 单元测试(又称为模块测试, Unit Testing)是针对程序模块(软件设计的最小...
    海波笔记阅读 4,955评论 0 52
  • 偶尔会遇到通过网络请求的url中带中文的情况,这样在加载图片的时候会导致加载不出来网上很多都是类似URLEncod...
    忘就忘了吧阅读 1,844评论 0 0
  • 今天在开发中遇到一个主线程的handler发送消息,发送出去一直没有接受到,没有处理的问题。 我的代码里面有个Ti...
    幸福的程序媛阅读 1,363评论 0 0