回顾

《谈谈为什么写单元测试》

基本单元测试框架

Java单元测试框架:Junit、Mockito、Powermockito等;Android:Robolectric、AndroidJUnitRunner、Espresso等。

最开始建议先学习Junit & Mockito。这两款框架是java领域应用非常普及,使用简单,网上文章非常多,官网的说明也很清晰。junit运行在jvm上,所以只能测试纯java,若要测试依赖android库的代码,可以用mockito隔离依赖(下面会谈及)。

Junit官网
Mockito官网

之后学习AndroidJUnitRunner,Google官方的android单元测试框架之一,使用跟Junit是一样的,只不过需要运行在android真机或模拟器环境。由于mockito只在jvm环境生效,而android是运行在DalvikART,所以AndroidJUnitRunner不能使用mockito

然后可以尝试Robolectric & EspressoRobolectric运行在jvm上,但是框架本身引入了android依赖库,所以可以做android单元测试,运行速度比运行在真机or模拟器快。但Robolectric也有局限性,例如不支持加载so,测试代码也有点别扭。当然,robolectric可以配合junit、mockito使用。Espresso也是Google官方的android单元测试框架之一,强大就不用说了,测试代码非常简洁。Espresso本身运行在真机上,因此android任何代码都能运行,不像junit&mockito那样隔离依赖。缺点也是显而易见,由于运行在真机,不能避免“慢”

Robolectric官网
Android-testing-support-library官网

其实espresso应该是几款框架中最简单的,但笔者还是建议先学习junit&mockito。因为新手很可能会因为espresso的强大、简单,而忽略了junit做单元测试带来的巨大意义。例如,前文提到“快速定位bug”、“提高代码质量”,espresso慢,有违“快速”;用espresso不用修改工程任何代码,这不利于提高代码质量。

本文主要介绍junitmockito,以及单元测试一些重要概念。


Junit

先给大家上两段代码压压惊:

public class Calculater {public int add(int a, int b) {return a + b;}
}

AssertEquals

单元测试用例:

public class CalculaterTest {Calculater calculater = new Calculater();@org.junit.Testpublic void testAdd() {int a = 1;int b = 2;int result = calculater.add(a, b);Assert.assertEquals(result, 3); // 验证result==3,如果不正确,测试不通过}
}

以上是一个要测试的类Calculater和测试用例CalculaterTest。在IntellijAndroid Studio对类右键->run CalculaterTest,用例中所有被@org.junit.Test注解的方法,就会被执行。

run calculaterTest

pass

测试通过。

如果代码改成Assert.assertEquals(result, 4);,测试会失败。

not pass

Verify

verify的作用,是验证函数是否被调用(以及调用了多少次)。

public class CalculaterTest {@org.junit.Testpublic void testAdd2() {calculater = mock(Calculater.class);calculater.add(1, 2);verify(calculater).add(1, 2); // 验证calculater.add(a, b)是否被调用过,且a==1 && b==2// 测试通过}
}

是不是很简单?

Mockito

官网这样描述:

Mockito is a mocking framework that tastes really good. It lets you write beautiful tests with a clean & simple API.

大概意思是,Mockito是一个体验很好的mocking框架,它可以让你写出漂亮、简洁的测试代码。

什么是mocking?下文会详细说明。不如先让你感受一下mockito代码:

public interface IMathUtils {public int abs(int num); // 求绝对值
}
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;public class MockTest {public static void main(String[] args) {IMathUtils mathUtils = mock(IMathUtils.class); // 生成mock对象when(mathUtils.abs(-1)).thenReturn(1); // 当调用abs(-1)时,返回1int abs = mathUtils.abs(-1); // 输出结果 1Assert.assertEquals(abs, 1);// 测试通过}
}

可以发现IMathUtils是一个接口,根本就没有实现,用Mockito框架mock之后,IMathUtils.abs(-1)就有返回值1了。这就是Mockito神奇的地方!Mockito代理了IMathUtils.abs(num)的行为,只要调用时符合指定参数(代码中指定参数-1),就可以得到映射的返回值

Mockito的语法when...thenReturn...相当直观,只要你小学有学英语_都能看懂。

读者肯定认为Mockito用了Java代理,实际上要更高级一点,Mockito底层用了CGLib(github/cglib)做动态代理。


依赖隔离

依赖隔离,这是单元测试中一个非常重要的概念。一个单元的代码,通常会有各种依赖。写单元测试时,应该把这些依赖隔离,让每个单元保持独立。举个例子:

public class Calculater {public double divide(int a, int b) {// 检测被除数是否为0if (MathUtils.checkZero(b)) {throw new RuntimeException("dividend is zero");}return (double) a / b;}
}
public class MathUtils {public static boolean checkZero(int num) {return num == 0;}
}

divide(a,b)计算a除以b,但被除数b不应该为0,所以用MathUtils.checkZero(b)验证b==0。咋看这里好像没什么问题,但是,如果MathUtils.checkZero里面的判断逻辑写错呢?例如:

 public static boolean checkZero(int num) {return  num != 0; // bug}

如果不是num==0那么简单,而是更复杂的算法呢?

因为Calculater引用的任何依赖,都可能出错。更糟糕的是,如果用junit做单元测试,依赖里面可能是Android库或者jni native方法,依赖方法一执行就会报错。以上的各种原因,都会影响单元测试的结果。所以,我们对代码做如下改进:

public class Calculater {IMathUtils mathUtils;public double divide(int a, int b) {if (mathUtils.checkZero(b)) {throw ...}return (double) a / b;}
}
public interface IMathUtils {public boolean checkZero(int num);
}

我们可以在Calculater构造方法传入IMathUtils派生类,又或者用setter。在项目执行代码中,传MathUtils,而单元测试时,可以写一个MathUtilsTest继承IMathUtils,传给Calculater。只要保证MathUtilsTest.checkZero()正确就行。经过这么重构,Calculater就不依赖原来的MathUtils,单元测试时可以替换专门的实现,达到了依赖隔离的目的

有同学会问,这样岂不是每个依赖都要写一个专门给单元测试的类吗?这就等于拷贝多一份代码,并且写各种接口,而且不能保证单元测试的类一定正确。

说得很有道理。笔者为了尽量简单地演示代码,举了一个非常简单的例子。我们如何让单元测试更简洁,并且让它阅读起来更有意义呢?

Mock

为了更好地解决上述问题,我们引入Mock概念。Mock,翻译为模拟,在单元测试mock可以模拟返回数据,也可以模拟接口/方法的行为

什么是模拟行为?例如刚才mathUtils.checkZero(b),意义为:“当mathUtils调用checkZero(num)”时,判断 num==0;又或者:“当调用checkZero(0)时返回truenum为其他值时返回false”。checkZero()是一个行为,返回的true、false就是数据。单元测试时,我们经常要让方法/接口模拟某些行为,并得到模拟数据

例如,需要测试a=2,b=1a=2,b=0调用divide(a,b)两者结果分别是2,抛出错误,使用mockito框架让mathUtils.checkZero()模拟行为,代码如下:

public static void main(String[] args) {// 生成IMathUtils模拟对象IMathUtils mathUtils = mock(IMathUtils.class);when(mathUtils.checkZero(1)).thenReturn(false); // 当num==1时,checkZero(num)返回falsewhen(mathUtils.checkZero(0)).thenReturn(true); // 当num==0时,checkZero(num)返回trueCalculater calculater = new Calculater(mathUtils);assertEquals(calculater.divide(2,1), 2); // 验证 divide(2,1) 结果是2try {calculater.divide(2, 0); // 预期抛出错误throw new RuntimeException("no expectant exception"); // 如果divide没抛错,则此处抛错} catch (Exception e) {Assert.assertEquals(e.getMessage(), "dividend is zero"); // 验证错误信息}
}

这段测试代码可以运行通过!

代码剖析:

  • mock(IMathUtils.class)生成IMathUtils类的模拟对象(称mock对象)。这个mock对象调用任何方法都不会被实际执行;
  • when(mathUtils.checkZero(1)).thenReturn(false),当调用checkZero(num)并且num==1,返回false,这里mockito模拟了checkZero()行为,并模拟了返回数据;
  • 所以,calculater.divide(2,1)返回结果2,calculater.divide(2, 0)抛出RuntimeException。

以上例子描述了,使用mockito模拟类方法和返回数据,通过mock隔离了CalculaterIMathUtils实现类的依赖,并通过单元测试,验证了divide()的逻辑正确性。

条件覆盖

无限条件

要验证程序正确性,必然要给出所有可能的条件(极限编程),并验证其行为或结果,才算是100%覆盖条件。实际项目中,验证边界条件和一般条件就OK了。

还是上面那个例子,只给出两个条件:a=2,b=1a=2,b=0a=2,b=1是一般条件,b=0是边界条件,还有一些边界条件a=NaN,b=NaN等。要验证除法正确性,恐怕得给出无限的条件,实际上,只要验证几个边界条件和一般条件,基本认为代码是正确了。

有限条件

再举个例子:stateA='a0'、'a1', stateB='b0'、'b1'、'b2',根据stateAstateB不同组合输出不同结果,例如a0b0输出a0b0a0b1输出a0b1,所以,共2*3=6种情况。这时,并不存在边界条件,所以条件都是特定条件,并且条件有限。

这种情况在项目中很常见,以笔者经验,建议单元测试时把所有情况都验证一遍,确保没有遗漏。


单元测试不是集成测试

集成测试

集成测试,也叫组装测试、联合测试。在单元测试的基础上,将相关模块组合成为子系统或系统进行测试,称为集成测试。通俗一点,集成测试就是把多个(最少2个)组件合在一起,测试某个功能片段,甚至是单独功能。

单元测试仅针对单元

在微信群很多同学问:“用Robolectric能不能请求网络”,"Junit能直接请求服务器吗"?

例如,我们使用MVP模式,如果我们想测试:调用PresenterA接口,请求真实网络,并且返回数据后,解析成对象,并且根据返回数据执行对应逻辑。这明显就是一个集成测试,而不是单元测试。PresenterA是一个单元,M层的Repository、DAO等是一个单元,更底层的sqlite第三方库、网络请求第三方库(okhttp等) 也是单元.....组合了n个单元的测试,是集成测试

Robolectric、Junit能否请求网络?

包括笔者在内,很多同学一开始都会有这个疑问。

阅读了本文第一部分,应该了解到robolectric、junit是运行在jvm,只要有一点点java开发经验的同学,都知道jvm本身能连接网络。如果你调用的方法所依赖的一切代码,都不依赖Android库(例如okhttp、retrofit),那99%都能在jvm上跑,并且能请求服务器。如果不幸有Android依赖,很大概率还是能在robolectric上跑的。

为什么robolectric不是100%能跑通测试呢?Robolectric仅支持API21及以下,并且不支持jni库。因此,如果你的代码依赖了API21以上接口或者jni接口,robolectric也无能为力。天啊!怎么办?

请读者先不要沮丧,我们自有对策,不过要看读者慧根了_!。前文“依赖隔离”提到,我们可以通过一定手段,把jni、android依赖隔离掉。咦?咱们的代码是不是有救了?之后的文章,笔者会详细给大家讲解一下。

单元测试才是必要的

经过笔者指点,可能有读者蠢蠢欲动去尝试集成测试了.....且慢!说好的单元测试呢?集成测试看起来简单,实际上由于依赖过多,很多时候很麻烦,而且运行慢;相比之下,单元测试则小巧、灵活得多,运行快,快速发现bug。在这方面,有一个理论Test Pyramid

Test Pyramid

示意图中,左箭头表示速度,右箭头表示开发成。可以看到,单元测试速度比集成测试(Service,也叫Integration)、UI测试要快,并且开发成本也是最低。Test Pyramid告诉我们,应该花大部分精力去写单元测试,其次才是集成测试、UI测试。

笔者建议,还是先老老实实做单元测试,有时间精力再做集成测试。


小结

本文介绍了几个单元测试框架,介绍了junit、mockito初步使用,阐述了依赖隔离、mocking的概念,解答了"robolectric、junit能否请求网络"问题。结合阅读《谈谈为什么写单元测试》,想必读者对单元测试有了一个初步的了解。

如果读者问笔者:“我的是小项目,是否有必要做单元测试?” 我很肯定地回答,任何项目都有必要做单元测试。至于单元测试是否耗费很多时间,或者效果不显著,这要看使用者的编程经验了,不能一概而论。

最后,叮嘱读者多敲代码,真枪实弹地实践单元测试。可以从公司项目小规模使用,形成自己单元测试风格后,就可以跟大范围地推广了。欢迎在本文留言讨论!

下一篇:《Android单元测试 - 几个重要问题》


关于作者

我是键盘男。
在广州生活,在互联网公司上班,猥琐文艺码农。喜欢科学、历史,玩玩投资,偶尔独自旅行。

Android单元测试 - 如何开始?相关推荐

  1. Android单元测试全解

      自动化测试麻烦吗?说实在,麻烦!有一定的学习成本.但是,自动化测试有以下优点: 节省时间:可以指定测试某一个activity,不需要一个个自己点 单元测试:既然Java可以进行单元测试,Andro ...

  2. Android 单元测试学习计划

    网上查了一下Android单元测试相关的知识点,总结了一个学习步骤: 1. 什么是单元测试 2. 单元测试正反面: 2.1. 重要性 2.2. 缺陷 2.3. 策略 3. 单元测试的基础知识: 3.1 ...

  3. Android单元测试 - 几个重要问题

    前言 已经一个月没写文章了,由于9月份在plan国庆旅行计划,国庆前前后后去了14天旅行,所以没时间写,哈哈. 言归正传,上一篇文章<Android单元测试 - 如何开始?>介绍了几款单元 ...

  4. Android单元测试框架Robolectric3.0介绍(二)

    文章中的所有代码在此:https://github.com/geniusmart/LoveUT ,由于 Robolectric 3.0 和 3.1 版本(包括后续3.x版本)差异不小,该工程中包含这两 ...

  5. Android单元测试研究与实践

    处于高速迭代开发中的Android项目往往需要除黑盒测试外更加可靠的质量保障,这正是单元测试的用武之地.单元测试周期性对项目进行函数级别的测试,在良好的覆盖率下,能够持续维护代码逻辑,从而支持项目从容 ...

  6. Android单元测试 - Sqlite、SharedPreference、Assets、文件操作 怎么测?

    前言 上篇<Android单元测试 - 几个重要问题> 讲解了"何解决Android依赖.隔离Native方法.静态方法.RxJava异步转同步"这几个Presente ...

  7. android单元测试作用,Android单元测试源码解读

    Android手机操作系统是一个开源的操作系统.程序员们可以在模拟器的帮助下对其进行修改,来实现各种功能需求,满足用户的应用.在这里我们先来了解一下Android单元测试的相关内容. 在网络上找了半天 ...

  8. android单元测试android环境,基于Robolectric的Android单元测试 —环境搭建与部署运行...

    移动端的测试中,因为回归一些逻辑分支比较多的功能时工作量比较大,且不太适合用UI完成,尝试通过单元测试来完成.几经波折终于完成了一个功能的UT用例并在CI上部署运行,现总结如下: 一.Robolect ...

  9. Android单元测试 Instrumentation

    开发中我们需要对部分功能进行单元测试,启动Activity来测试部分小功能,有点小题大作,杀鸡用牛刀. 我们可以用Android单元测试 Instrumentation 本篇只是入门,起到抛砖的效果 ...

  10. (转)Android单元测试

    关键字: camera unit test android源代码中每个app下中都自带了一个test用例,下面主要介绍下camra单元测试用例 在AndroidManifest.xml中标明了测试用例 ...

最新文章

  1. 2022-2028年中国康养地产行业市场需求前景及投资战略分析报告
  2. 完整的虚幻引擎超级课程:从初学者到专家
  3. 基因组组装(Genome Assembly)
  4. 大型互联网公司必考java面试题与面试技巧
  5. SAMBA服务和FTP服务讲解(week3_day1)--技术流ken
  6. mysql8集群的优点_介绍 MySQL 8 中值得关注的新特性和改进。
  7. WebRTC大会火爆上演,网易云信谈音视频研发三大突破点
  8. java序列化Serializable
  9. sublimelinter-php 错误代码提示
  10. 计算机等级考试真题演示,全国计算机等级考试二级真题测试(答案)四、演示文稿题-日...
  11. PHP生成随机数;订单号唯一
  12. Google 向平板电脑彻底说再见!
  13. python 全栈开发,Day54(关于DOM操作的相关案例,JS中的面向对象,定时器,BOM,client、offset、scroll系列)...
  14. BUUCTF misc 专题(77)间谍启示录
  15. 虚拟机16.2去虚拟化教程 超详细全面讲解
  16. run.gps+trainer+uv+for+android,android 2.1(三星spica i5700)上的蓝牙问题配对工作但连接不起作用...
  17. android软件多版本共存,1. android studio 多版本共存(2.2 and 3.3)
  18. dll占的究竟是谁的空间?——浅谈Windows内存机制
  19. eclipse中几种加入jar包方式的区别
  20. 1-JVM之Java运行时数据区域

热门文章

  1. Matlab的数据导入和导出
  2. hd6630m可以玩lol吗_《LOL》流畅玩!Intel HD620核显性能实测
  3. Ios 原生开发笔记
  4. Axure统计图表设计
  5. word单页(或中间几页)横向显示
  6. html中表格table冻结行和列
  7. 嵌入式linux触摸屏校正命令,[转]基于嵌入式Linux的通用触摸屏校准程序
  8. 消元法解二元一次方程组c语言,消元法解二元一次方程组的概念、步骤与方法...
  9. WPS设置奇偶页页眉不同
  10. 求余函数mod和fmod