单元测试学习笔记

示例代码太少,之后会逐渐补上。html

目录:java

综述

若是你查过一些关于单元测试的资料,你可能会和我同样发现一个问题。有一些文章在说到单元测试的时候,提到了要作边界测试,要考虑各类分支;也有一些文章则说的是修改原有代码,例如依赖隔离;还有一些文章说的是测试的框架的使用,例如 Junit 。那么它们之间有着什么样的联系呢?android

最开始,咱们可能更关注边界测试和分支测试。但遗憾的是,这方面的资料相对来讲较少。更多的是依赖隔离这类的文章。为何?segmentfault

由于有不少代码是没法被测试的。安全

可以被测试的代码须要知足某些条件。你可能会以为很麻烦,作单元测试还要为了知足这些条件去修改原来的代码。事实上,知足这些条件能使你的代码变得健壮。若是你写的代码是没法被测试的,那么你的首要任务就是将它们重构为可测试的单元。要想知道如何写出可测试的代码,就得了解 Mock 和 Stub 。这也就是你在看一些加减乘除单元测试例子以后,仍然不知道怎么测试本身的代码的缘由。(每次看到这样的文章就好气啊_(:з」∠)_)网络

可是即便重构了,还有一个问题。你总得写代码来执行对其余代码进行测试吧?这部分的代码可能很复杂,也可能变得难以维护。因而测试框架就出现了,帮你减轻作测试的负担。数据结构

简单说,它们三者之间的关系是:先重构已有代码,使其成为可测试的单元,为接下去的测试作准备。接着写出对这些单元进行测试的代码,验证结果。为了使测试代码易于编写和维护,借助测试框架。框架

单元测试时所面临的问题

为了使代码可被测试,须要对其进行重构。在这个过程当中会遇到一些问题:ide

  • 一个类的方法里包含了其余类的方法,怎么测试?
  • 若是代码依赖于 Web 服务,例如请求某个网站的数据,怎么测试?
  • 一个类的方法里包含了该类的其余方法,要怎么测试?
  • 一个类的方法有不少个对数据处理的步骤,是要测试最终结果,仍是要对每一个处理的步骤可能出现的问题进行测试?
  • 一个方法没有返回值(即 void )怎么办?——交互测试

对于前两个问题,能够用依赖隔离来解决。《单元测试的艺术》的3.1有个用来理解依赖隔离的例子:
航天飞机在送入太空以前,要先进行测试,不然飞到一半出了问题怎么办?
而有一部分测试是确保宇航员已经准备好进入太空。可是你又不能让宇航员真的坐实际的航天飞机去测试。有个办法就是建造一套仿真系统,模拟航天飞机控制台的环境。输入某种特定的外部环境,而后看宇航员是否执行了正确的操做。
在这个例子中,经过模拟外部环境来解除了对实际外部环境(航天飞机进入太空)的依赖。一样的思路能够用到测试中。函数

依赖隔离

先从写出可以测试的代码开始提及吧。

参考文章:Android单元测试 - 如何开始?

这里的依赖指的是:当前 类A 须要调用 类B 的方法,则说 A 依赖于 B 。

隔离方法:

  1. 将 B 改为一个 接口C 。
  2. 将 A 中的 B类 出现的位置替换为 接口C 。

A 和 B 隔离先后对比:
隔离前:A -> B
隔离后:A -> C -> B

在项目实际代码以及测试代码中使用不一样的B:

  • 在项目执行代码中:传入 类A 的对象是 接口C 的一个 派生类D (实现了 接口C )。 类D 是 项目中实际运行的代码,提供了对接口的完整实现。A -> C -> D
  • 在单元测试的代码(独立于项目执行代码,发布软件时要把这部分删掉)中:传入 A 的对象也是实现了 接口C 的一个 派生类E 。可是这个类与D不一样,它提供的实现可能只是一个return。从而模拟(Mock)了派生类D的特定行为,使得在测试的时候,不须要使用D类。A -> C -> E

这样作的好处是,一旦隔离完成,之后就没必要大幅度修改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的时候,获取该信息,查看是否和预期的一致。

参考:

《单元测试的艺术》第四章

Android单元测试在蘑菇街支付金融部门的实践

单元测试框架

在测试以前,要建立一个专门用于测试的类。这个类的类名以Test结尾。在类里面添加测试方法,测试方法名前面要加上 test ,接在 test 后面的是被测试的方法名。在该方法内作三件事:

  1. 测试以前须要准备的数据,例如 new 出要测试的类——Setup
  2. 执行要测试的类的方法——Action
  3. 最后添加 Assert 以验证结果——Verify

测试框架里的 AssertXxx 是什么玩意儿?

咱们写的测试代码在运行的时候会产生一些结果,验证这些结果是否符合预期的一个低效方法就是将这些结果输出到控制台(Console)或者文件,而后你本身用眼睛一个个去对比。

若是你懒得去比呢?又或者说你对比的时候以为没错,可是其实是由于一个1l的错误致使你没有发现呢?

就让 Assert 来帮你解决这些烦人的问题吧! Assert (中文为:断言)就是让你将预期的结果和程序运行的结果传入它的方法里面,由它来替你作对比的事情。

例如一个测试结果是否相等的 Assert :
assertEquals(你本身算出的结果, 程序运行的结果);
若是两个结果不一样,即程序运行的结果不符合你的预期,那么它就会提示你这里出现了错误。

今后,你就从几百甚至是几万条的测试代码输出的对比中解放出来,大大节约了时间。

有些文章标题看着像是介绍单元测试,其实是介绍单元测试框架。测试框架(Junit,Nunit等)其实是提供便于测试的方案的框架,学习这些内容是学习框架的结构,以及如何使用框架定义的各类 assert ,而不是学习单元测试的方法。这二者要区分开来!!!!!

快捷实现用于测试接口的框架(Mockito)

对于刚才那个接口 IMathUtils ,咱们能够不用再新建一个类去实现它,而是交给 Mockito 。
IMathUtils mathUtils = mock(IMathUtils.class); // 生成mock对象

when(mathUtils.checkZero(1)).thenReturn(false); // 这里是快捷实现。它告诉 Mockito :若是在下面代码调用了mathUtils.checkZero()并传入参数1,那么就让调用这个方法的地方返回false这个值。

作好以上准备后

  1. 单元测试整体上须要作些什么?
    • 只考虑代码在最正确的操做和条件下可否得出正确的结果
    • 在数据的边界条件下可否获得正确的结果
    • 代码在全部可能的错误数据下可否给出错误提示或者不至于崩溃
    • 如何消除依赖隔离
  2. 单元测试的任务(摘自:Java单元测试(Junit+Mock+代码覆盖率)
    • 接口功能测试:用来保证接口功能的正确性。
    • 局部数据结构测试(不经常使用):用来保证接口中的数据结构是正确的
      • 好比变量有无初始值
      • 变量是否溢出
    • 边界条件测试
      • 变量没有赋值(即为NULL)
      • 变量是数值(或字符) 时
        • 主要边界:最小值,最大值,无穷大(对于 double 等)
        • 溢出边界(指望异常或拒绝服务):Min - 1,Max + 1
        • 临近边界:Min + 1,Max - 1
      • 变量是字符串时
        • 应用上面提到的字符变量的边界
        • 空字符串
        • 对字符串长度应用数值变量的边界
      • 变量是集合时
        • 空集合(Empty)
        • 对集合的大小应用数值变量的边界
        • 调整次序:升序、降序
      • 变量有规律时
        • 好比对于Math.sqrt,给出n^2-1,和n^2+1的边界
    • 全部独立执行通路测试:保证每一条代码,每一个分支都通过测试
      • 代码覆盖率
        • 语句覆盖:保证每个语句都执行到了
        • 断定覆盖(分支覆盖):保证每个分支都执行到
        • 条件覆盖:保证每个条件都覆盖到 true 和 false (即 if 、 while 中的条件语句)
        • 路径覆盖:保证每个路径都覆盖到
      • 相关软件
        • Cobertura:语句覆盖
        • Emma: Eclipse插件Eclemma
    • 各条错误处理通路测试:保证每个异常都通过测试
  3. Android 单元测试的任务(摘自:Android单元测试在蘑菇街支付金融部门的实践
    • 全部的Model、Presenter/ViewModel、Api、Utils等类的public方法
    • Data类除了getter、setter、toString、hashCode等通常自动生成的方法以外的逻辑部分
    • 自定义View的功能:好比set data之后,text有没有显示出来等等,简单的交互,好比click事件,负责的交互通常不测,好比touch、滑动事件等等。
    • Activity的主要功能:好比view是否是存在、显示数据、错误信息、简单的点击事件等。比较复杂的用户交互好比onTouch,以及view的样式、位置等等能够不测。由于很差测。

重构与单元测试

在单元测试前要重构,在重构前要编写集成测试。
集成测试 ——> 重构 ——> 单元测试
重构的过程当中,每次只作少许的改动。尽量多的运行集成测试,以此了解重构是否使得系统原有的功能被破坏。
要点:关注系统中你须要修复或者添加功能的部分,不要在其余部分浪费精力。其余部分等到须要处理的时候再考虑。

修复 BUG 或添加新功能的单元测试

先编写一个单元测试,这个测试针对于这个 BUG 。因为它是一个 BUG ,因此显然这个单元测试一开始给出的结果会是失败的。此时你修复 BUG ,并运行测试。若是测试成功,则表示你成功修复了这个 BUG ;若是测试失败,则表示 BUG 仍然存在。

换句话说,这个单元测试暴露了这个 BUG 。 BUG 原本没看出来,而这个单元测试的失败代表了 BUG 的存在。

添加新功能也是一样。写出一个会失败的测试,表示缺乏这个功能。而后经过修改代码使得测试经过,就代表你成功添加了新功能。

得到接口的几种方法(基于值和状态的测试)

在本篇的 MathUtils 例子中,经过setMathUtils()传入 IMathUtils 的实现。这是经过 getter 和 setter 对类的成员变量操做的方法。这种方法称为依赖/属性注入。除此以外,还有其余方法。

  • 在方法调用点注入伪对象(《单元测试的艺术》3.4.6)
    这种方法与属性注入须要先获取实例再传入不一样,它经过在构造函数里使用工厂方法获取实例。
    • 方案一:工厂类
      在被测试类的构造方法里执行了静态的工厂方法。不过工厂方法执行以前,经过 setter 传入用于测试的接口实现。这种方法与属性注入的不一样之处在于,将 set 方法移入另外建立的工厂类。在测试的时候你彻底不须要管被测试类,只须要对工厂类进行操做就能够。
      须要注意什么问题?你须要了解什么代码会在何时调用这个工厂,根据时机 set 进所需的工厂实现。
    • 方案二:本地工厂方法
      不使用工厂类,而是在被测试类里新建一个工厂方法。将被测试类设置为抽象类,完整地实现了除工厂方法外的全部方法,让子类继承并重写工厂方法。测试时有测试的实现,实际运行时有运行的实现。
      何时应该使用?模拟给被测试代码的输入。
      何时不该该使用?测试代码对服务的调用是否正确时。
      当被测试代码已是依赖隔离或者应用了属性注入的时候,不考虑。若没有,则优先考虑。
  • 构造函数注入,赋值给类的成员变量
    建立新的构造函数,或者在原有构造函数上添加参数。若是类须要注入多个依赖,则会下降代码的可读性和可维护性(构造函数的参数个数可能变化的通病)。
    • 优化方案一:参数对象重构。将参数整合为一个对象,传入该对象。
    • 优化方案二:控制反转。控制反转的一个例子是 JAVA 的反射机制,根据类名生成对象。控制反转能够看作是将工厂方法中的生成对象的代码改到 XML 文件中。
    • 何时使用?第一:使用控制反转容器的框架。第二:想告诉 API 使用者这些参数是必须的(若是是可选的,则使用 getter 和 setter )。
    • 须要注意什么问题?大多数人不知道什么是控制反转原则。这意味着你一旦写出方案二这样的代码,就须要在别人不懂的时候教他。
  • 把参数放到须要被测试的方法的参数列表里

一些补充

  1. 应该对哪些代码编写单元测试?哪些代码不太须要编写单元测试?
    不常常改动的代码,特别是底层核心的代码须要编写单元测试。常常改动的代码就不太须要编写单元测试。毕竟你刚写完单元测试不久,整个代码就被修改了,你得再从新编写单元测试。

  2. 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

    若是在一个单元测试中,验证了多个点,你可能没法知道究竟是哪一个点出了错。应该尽量分离。

  3. 想作单元测试结果作成集成测试
    若是既要请求网络,又要保存数据,还要显示界面,那就是集成测试了。

  4. 在使用断言确认字符串的时候,应该把整个预期字符串都写上么?
    在《重构:改善既有代码的设计》里面,有个测试读取文件的例子。

  5. 单元测试框架中的setup()
    • setup()方法应该初始化全部测试方法都须要的对象。至于只有某个测试方法用到的对象,交给这个测试方法来初始化。
    • 防止过分重构setup()方法。在重构时征求同伴的意见。
    • 不要在setup中准备伪对象。
  6. 还须要注意什么?
    • 编写测试时,要时刻考虑到阅读测试的人。想象一下他们第一次读到代码时的情形,确保他们不会生气。
    • 一个单元测试方法不能调用另一个单元测试方法。若是想删除重复代码,那就抽取共同的代码到另外一个方法中。
    • 隔离测试的方法:把你当前正在写的测试当作系统中惟一的一个测试。可是要注意,你必须把单元测试可能修改的状态恢复到初始值
    • 最安全的作法:每一个测试使用一个单独的对象实例。
    • 想要进行流测试,最好使用某种集成测试框架。
    • 断言和操做须要分离。在断言的参数里面,只传入最终结果,其方法调用过程须要分离开。

参考:

相关文章
相关标签/搜索