原文连接:http://www.jianshu.com/p/bc99678b1d6e
做者:键盘男kkmike999html
回顾:java
《谈谈为何写单元测试》android
Java单元测试框架:Junit、Mockito、Powermockito等;Android:Robolectric、AndroidJUnitRunner、Espresso等。git
最开始建议先学习Junit & Mockito。这两款框架是java领域应用很是普及,使用简单,网上文章很是多,官网的说明也很清晰。junit运行在jvm上,因此只能测试纯java,若要测试依赖android库的代码,能够用mockito隔离依赖(下面会谈及)。github
以后学习AndroidJUnitRunner,Google官方的android单元测试框架之一,使用跟Junit是同样的,只不过须要运行在android真机或模拟器环境。因为mockito只在jvm环境生效,而android是运行在Dalvik或ART,因此AndroidJUnitRunner不能使用mockito。sql
而后能够尝试Robolectric & Espresso。Robolectric运行在jvm上,可是框架自己引入了android依赖库,因此能够作android单元测试,运行速度比运行在真机or模拟器快。但Robolectric也有局限性,例如不支持加载so,测试代码也有点别扭。固然,robolectric能够配合junit、mockito使用。Espresso也是Google官方的android单元测试框架之一,强大就不用说了,测试代码很是简洁。Espresso自己运行在真机上,所以android任何代码都能运行,不像junit&mockito那样隔离依赖。缺点也是显而易见,因为运行在真机,不能避免“慢”。编程
其实espresso应该是几款框架中最简单的,但笔者仍是建议先学习junit&mockito。由于新手极可能会由于espresso的强大、简单,而忽略了junit作单元测试带来的巨大意义。例如,前文提到“快速定位bug”、“提升代码质量”,espresso慢,有违“快速”;用espresso不用修改工程任何代码,这不利于提升代码质量。微信
本文主要介绍junit和mockito,以及单元测试一些重要概念。
先给你们上两段代码压压惊:
public class Calculater { public int add(int a, int b) { return a + b; } }
单元测试用例:
public class CalculaterTest { Calculater calculater = new Calculater(); @org.junit.Test public void testAdd() { int a = 1; int b = 2; int result = calculater.add(a, b); Assert.assertEquals(result, 3); // 验证result==3,若是不正确,测试不经过 } }
以上是一个要测试的类Calculater
和测试用例CalculaterTest
。在Intellij
或Android Studio
对类右键
->run CalculaterTest
,用例中全部被@org.junit.Test
注解的方法,就会被执行。
测试经过。
若是代码改为Assert.assertEquals(result, 4);
,测试会失败。
verify
的做用,是验证函数是否被调用(以及调用了多少次)。
public class CalculaterTest { @org.junit.Test public void testAdd2() { calculater = mock(Calculater.class); calculater.add(1, 2); verify(calculater).add(1, 2); // 验证calculater.add(a, b)是否被调用过,且a==1 && b==2 // 测试经过 } }
是否是很简单?
官网这样描述:
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)时,返回1 int abs = mathUtils.abs(-1); // 输出结果 1 Assert.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) { // 检测被除数是否为0 if (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能够模拟返回数据,也能够模拟接口、类的行为。
什么是模拟行为?例如刚才mathUtils.checkZero(b)
,意义为:“当mathUtils
调用checkZero(num)
”时,判断 num==0
;又或者:“当调用checkZero(0)
时返回true
,num
为其余值时返回false
”,返回的true、false
就是模拟数据。
例如,须要测试a=2,b=1
和a=2,b=0
调用divide(a,b)
二者结果分别是2,抛出错误
,使用mockito框架mock mathUtils.checkZero()的行为,代码以下:
public static void main(String[] args) { // 生成IMathUtils模拟对象 IMathUtils mathUtils = mock(IMathUtils.class); when(mathUtils.checkZero(1)).thenReturn(false); // 当num==1时,checkZero(num)返回false when(mathUtils.checkZero(0)).thenReturn(true); // 当num==0时,checkZero(num)返回true Calculater calculater = new Calculater(mathUtils); assertEquals(calculater.divide(2,1), 2); // 验证 divide(2,1) 结果是2 try { 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隔离了Calculater
对IMathUtils
实现类的依赖,并经过单元测试,验证了divide()
的逻辑正确性。
要验证程序正确性,必然要给出全部可能的条件(极限编程),并验证其行为或结果,才算是100%覆盖条件。实际项目中,验证边界条件和通常条件就OK了。
仍是上面那个例子,只给出两个条件:a=2,b=1
和a=2,b=0
,a=2,b=1
是通常条件,b=0
是边界条件,还有一些边界条件a=NaN,b=NaN
等。要验证除法正确性,恐怕得给出无限的条件,实际上,只要验证几个边界条件和通常条件,基本认为代码是正确了。
再举个例子:stateA='a0'、'a1', stateB='b0'、'b1'、'b2'
,根据stateA
、stateB
不一样组合输出不一样结果,例如a0b0
输出a0b0
,a0b1
输出a0b1
,因此,共2*3=6种状况。这时,并不存在边界条件,因此条件都是特定条件,而且条件有限。
这种状况在项目中很常见,以笔者经验,建议单元测试时把全部状况都验证一遍,确保没有遗漏。
集成测试,也叫组装测试、联合测试。在单元测试的基础上,将相关模块组合成为子系统或系统进行测试,称为集成测试。通俗一点,集成测试就是把多个(最少2个)组件合在一块儿,测试某个功能片断,甚至是单独功能。
在微信群不少同窗问:“用Robolectric能不能请求网络”,"Junit能直接请求服务器吗"?
例如,咱们使用MVP模式,若是咱们想测试:调用PresenterA接口,请求真实网络,而且返回数据后,解析成对象,而且根据返回数据执行对应逻辑。这明显就是一个集成测试,而不是单元测试。PresenterA是一个单元,M层的Repository、DAO等是一个单元,更底层的sqlite第三方库、网络请求第三方库(okhttp等) 也是单元.....组合了n个单元的测试,是集成测试。
包括笔者在内,不少同窗一开始都会有这个疑问。
阅读了本文第一部分,应该了解到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:
示意图中,左箭头表示速度,右箭头表示开发成。能够看到,单元测试速度比集成测试(Service,也叫Integration)、UI测试要快,而且开发成本也是最低。Test Pyramid告诉咱们,应该花大部分精力去写单元测试,其次才是集成测试、UI测试。
笔者建议,仍是先老老实实作单元测试,有时间精力再作集成测试。
本文介绍了几个单元测试框架,介绍了junit、mockito初步使用,阐述了依赖隔离、mocking的概念,解答了"robolectric、junit可否请求网络"问题。结合阅读《谈谈为何写单元测试》,想必读者对单元测试有了一个初步的了解。
若是读者问笔者:“个人是小项目,是否有必要作单元测试?” 我很确定地回答,任何项目都有必要作单元测试。至于单元测试是否耗费不少时间,或者效果不显著,这要看使用者的编程经验了,不能一律而论。
最后,叮嘱读者多敲代码,真枪实弹地实践单元测试。能够从公司项目小规模使用,造成本身单元测试风格后,就能够跟大范围地推广了。欢迎在本文留言讨论!