示例代码太少,之后会逐渐补上。html
目录:java
若是你查过一些关于单元测试的资料,你可能会和我同样发现一个问题。有一些文章在说到单元测试的时候,提到了要作边界测试,要考虑各类分支;也有一些文章则说的是修改原有代码,例如依赖隔离;还有一些文章说的是测试的框架的使用,例如 Junit 。那么它们之间有着什么样的联系呢?android
最开始,咱们可能更关注边界测试和分支测试。但遗憾的是,这方面的资料相对来讲较少。更多的是依赖隔离这类的文章。为何?segmentfault
由于有不少代码是没法被测试的。安全
可以被测试的代码须要知足某些条件。你可能会以为很麻烦,作单元测试还要为了知足这些条件去修改原来的代码。事实上,知足这些条件能使你的代码变得健壮。若是你写的代码是没法被测试的,那么你的首要任务就是将它们重构为可测试的单元。要想知道如何写出可测试的代码,就得了解 Mock 和 Stub 。这也就是你在看一些加减乘除单元测试例子以后,仍然不知道怎么测试本身的代码的缘由。(每次看到这样的文章就好气啊_(:з」∠)_)网络
可是即便重构了,还有一个问题。你总得写代码来执行对其余代码进行测试吧?这部分的代码可能很复杂,也可能变得难以维护。因而测试框架就出现了,帮你减轻作测试的负担。数据结构
简单说,它们三者之间的关系是:先重构已有代码,使其成为可测试的单元,为接下去的测试作准备。接着写出对这些单元进行测试的代码,验证结果。为了使测试代码易于编写和维护,借助测试框架。框架
为了使代码可被测试,须要对其进行重构。在这个过程当中会遇到一些问题:ide
其余类
的方法,怎么测试?该类的其余方法
,要怎么测试?对于前两个问题,能够用依赖隔离来解决。《单元测试的艺术》的3.1有个用来理解依赖隔离的例子:
航天飞机在送入太空以前,要先进行测试,不然飞到一半出了问题怎么办?
而有一部分测试是确保宇航员已经准备好进入太空。可是你又不能让宇航员真的坐实际的航天飞机去测试。有个办法就是建造一套仿真系统,模拟航天飞机控制台的环境。输入某种特定的外部环境,而后看宇航员是否执行了正确的操做。
在这个例子中,经过模拟外部环境来解除了对实际外部环境(航天飞机进入太空)的依赖。一样的思路能够用到测试中。函数
先从写出可以测试的代码开始提及吧。
参考文章:Android单元测试 - 如何开始?
这里的依赖指的是:当前 类A 须要调用 类B 的方法,则说 A 依赖于 B 。
隔离方法:
A 和 B 隔离先后对比:
隔离前:A -> B
隔离后:A -> C -> B
在项目实际代码以及测试代码中使用不一样的B:
这样作的好处是,一旦隔离完成,之后就没必要大幅度修改A。在隔离的时候,要将全部依赖项改成从外部传入。这就须要给类A添加一个set方法,传入接口C的实现(implement),即上面的D和E。
类A:
public class Calculater { public double divide(int a, int b) { // 检测被除数是否为0 if (MathUtils.checkZero(b)) { throw new RuntimeException("divisor must not be zero"); } return (double) a / b; } }
它调用了类B(MathUtils)的 checkZero 方法。因而咱们说类A依赖于类B的 checkZero 方法。须要注意的是这个 MathUtils 不是从外部传入的 。
类B是一个具体实现的类:
public class MathUtils { public static boolean checkZero(int num) { return num == 0; } }
在知道产生依赖以后,要将类B改为一个接口(方法名前缀I表示这是一个接口Interface):
public interface IMathUtils { public boolean checkZero(int num); }
在类A的代码中,将B替换成该接口:
public class Calculater { private IMathUtils mMathUtils = new MathUtils(); // 这里的代码改动了 // 这里添加了set方法。向该类传入了mathUtils public void setMathUtils(IMathUtils mathUtils){ mMathUtils = mathUtils; } public double divide(int a, int b) { if (mMathUtils.checkZero(b)) { // 这里的代码改动了,将静态类改为对象 throw new RuntimeException("divisor must not be zero"); } return (double) a / b; } }
以前的B是一个静态类,不须要声明,但改为接口后须要声明。
接口的实现:
对于实际运行的代码,须要一个类去实现 IMathUtils 接口,而后传入 Calculater 。
修改类B:
JAVA public class MathUtils implements IMathUtils{ public boolean checkZero(int num) { return num == 0; } }
对于用于测试的代码,也须要一个类实现 IMathUtils 接口,而后传入 Calculater 。但不一样的是,这个类的实现可能只需添加一个 return 语句,不用细致实现。
老是正确的接口:
JAVA public class FakeMathUtils implements IMathUtils{ public boolean checkZero(int num) { return true; } }
return的时候,能够设一个变量,方便配置不一样取值,不然还得建立新的类。
```JAVA
public class FakeMathUtils implements IMathUtils{
public boolean isZero = true;
public boolean checkZero(int num) { return isZero; }
}
```
若是一个特定的工做单元的最终结果是调用一个第三方对象(而不是返回某个值,即 void ),你就须要进行交互测试。
这个第三方对象不受你的控制。你没法查看它的源代码,或者这不是你负责测试的部分。所以你只需确保传给它的参数是正确的就能够了。
那么如何确保传过去的参数是正确的?
在这以前,要确保已经依赖隔离。
假设接口为:
public interface IPerson { ... public void doSomethingWithData(String data); }
待测试类的某个方法:
public class A { private String data = ""; ... public void methodA(IPerson person) { ... person.doSomethingWithData(data); } public void setData(String data) { this.data = data; } }
真正使用的 Person 类是如何实现的呢?假设咱们无从得知。咱们的任务是保证传入的 data 是符合咱们预期的。只要传入的内容符合预期,那么就说明咱们要测试的方法是没问题的。
伪实现:
public class FakePerson implements IPerson { private String data = ""; ... public void doSomethingWithData(String data) { this.data = data; } public String getData() { return data; } }
在调用 methodA 的时候,传入 FakePerson 实例。
A test = new A(); test.setData("hahahaha"); IPerson fakePerson = new FakePerson(); test.methodA(fakePerson); Assert.AssertEquals("hahahaha", fakePerson.getData());
伪对象 FakePerson 在被 测试类A 的 methodA 方法中调用,该方法会给伪对象传入某个信息。
伪对象 FakePerson 不对该信息进行进一步处理,只是赋值给类成员变量存储起来。
因为伪对象是从外部传入的 test.methodA(fakePerson);
,所以能够直接在外部获取存储的信息 fakePerson.getData()
。在assert的时候,获取该信息,查看是否和预期的一致。
参考:
《单元测试的艺术》第四章
在测试以前,要建立一个专门用于测试的类。这个类的类名以Test
结尾。在类里面添加测试方法,测试方法名前面要加上 test ,接在 test 后面的是被测试的方法名。在该方法内作三件事:
Setup
Action
Verify
测试框架里的 AssertXxx 是什么玩意儿?
咱们写的测试代码在运行的时候会产生一些结果,验证这些结果是否符合预期的一个低效方法就是将这些结果输出到控制台(Console)或者文件,而后你本身用眼睛一个个去对比。
若是你懒得去比呢?又或者说你对比的时候以为没错,可是其实是由于一个1
和l
的错误致使你没有发现呢?
就让 Assert 来帮你解决这些烦人的问题吧! Assert (中文为:断言)就是让你将预期的结果和程序运行的结果传入它的方法里面,由它来替你作对比的事情。
例如一个测试结果是否相等的 Assert :
assertEquals(你本身算出的结果, 程序运行的结果);
若是两个结果不一样,即程序运行的结果不符合你的预期,那么它就会提示你这里出现了错误。
今后,你就从几百甚至是几万条的测试代码输出的对比中解放出来,大大节约了时间。
有些文章标题看着像是介绍单元测试,其实是介绍单元测试框架。测试框架(Junit,Nunit等)其实是提供便于测试的方案的框架,学习这些内容是学习框架的结构,以及如何使用框架定义的各类 assert ,而不是学习单元测试的方法。这二者要区分开来!!!!!
对于刚才那个接口 IMathUtils ,咱们能够不用再新建一个类去实现它,而是交给 Mockito 。
IMathUtils mathUtils = mock(IMathUtils.class); // 生成mock对象
when(mathUtils.checkZero(1)).thenReturn(false); // 这里是快捷实现。它告诉 Mockito :若是在下面代码调用了mathUtils.checkZero()并传入参数1
,那么就让调用这个方法的地方返回false
这个值。
字符变量
的边界数值变量
的边界数值变量
的边界在单元测试前要重构,在重构前要编写集成测试。
集成测试 ——> 重构 ——> 单元测试
重构的过程当中,每次只作少许的改动。尽量多的运行集成测试,以此了解重构是否使得系统原有的功能被破坏。
要点:关注系统中你须要修复或者添加功能的部分,不要在其余部分浪费精力。其余部分等到须要处理的时候再考虑。
先编写一个单元测试,这个测试针对于这个 BUG 。因为它是一个 BUG ,因此显然这个单元测试一开始给出的结果会是失败的。此时你修复 BUG ,并运行测试。若是测试成功,则表示你成功修复了这个 BUG ;若是测试失败,则表示 BUG 仍然存在。
换句话说,这个单元测试暴露了这个 BUG 。 BUG 原本没看出来,而这个单元测试的失败代表了 BUG 的存在。
添加新功能也是一样。写出一个会失败的测试,表示缺乏这个功能。而后经过修改代码使得测试经过,就代表你成功添加了新功能。
在本篇的 MathUtils 例子中,经过setMathUtils()传入 IMathUtils 的实现。这是经过 getter 和 setter 对类的成员变量操做的方法。这种方法称为依赖/属性注入。除此以外,还有其余方法。
应该对哪些代码编写单元测试?哪些代码不太须要编写单元测试?
不常常改动的代码,特别是底层核心的代码须要编写单元测试。常常改动的代码就不太须要编写单元测试。毕竟你刚写完单元测试不久,整个代码就被修改了,你得再从新编写单元测试。
Mock/Stub
Mock 和 Stub 是两种测试代码功能的方法。 Mock 测重于对功能的模拟。 Stub 测重于对功能的测试重现。好比对于 List 接口, Mock 会直接对 List 进行模拟( assert 写在调用 List 的 test 方法里面);而 Stub 会新建一个实现了 List 的 TestList ,在其中编写测试的代码( assert 写在这个 TestList 里面)。《单元测试的艺术》4.2
Stub 不会使测试失败,仅仅是用来模拟各类场景。 Mock 相似 Stub ,但它还能使用 assert 。
优先选择 Mock 方式,由于 Mock 方式下,模拟代码与测试代码放在一块儿,易读性好,并且扩展性、灵活性都比 Stub 好。但须要注意,一个测试有多个 Stub 是可行的,但有多个 Mock 就会产生麻烦,由于多个 Mock 对象说明你同时测试了多个事情。编写测试代码时,不对 Stub 进行 assert ,而是统一到最后由 Mock 进行 assert 。若是你对明显是用作 Stub 的伪对象进行了断言,这属于过分指定。《单元测试的艺术》4.5
若是在一个单元测试中,验证了多个点,你可能没法知道究竟是哪一个点出了错。应该尽量分离。
想作单元测试结果作成集成测试
若是既要请求网络,又要保存数据,还要显示界面,那就是集成测试了。
在使用断言确认字符串的时候,应该把整个预期字符串都写上么?
在《重构:改善既有代码的设计》里面,有个测试读取文件的例子。
参考:
关于 Android 单元测试的一切 <-在页面里搜索该标题