好的单元测试

有效的单元测试

优秀测试如何提高生产力

  • 测试帮助我们捕获错误
  • 100% 的测试覆盖率只能保证你所有的代码都执行了,不论程序是否满足要求。
  • 我们不应该追求代码的覆盖率,而应该确保写出的测试有意义。
  • 我们不应该将测试只用作质量工具,而同时也应该用作设计工具,这样测试的价值会更高。
  • 为了完全发挥出测试的潜力:
    • 要像生产代码一样对待你的测试代码,大胆的重构、创建和维护高质量测试。
    • 开始将测试作为一种设计工具,知道代码针对实际用途进行设计。《测试驱动开发的艺术》《测试驱动的面向对象软件开发》
  • TDD
    • 测试驱动开发的过程,显示编写失败的测试,编写代码以通过测试,重构代码来改进设计。
    • TDD基于一个简单的想法:在编写出能够证明代码存在的失败测试之前,不写生产代码。这也是它有时被称为测试先行的原因。
    • 先写测试,会向测试所期望的方向来驱动生产代码的设计,这会带来以下好处:
      • 代码变得可用:你生产的代码使得测试通过,也即代码的设计和API 适合于你的使用场景
      • 代码变得精益:你的生产代码仅仅使得你的测试通过,也即仅仅实现场景所需要的功能。
    • TDD 的步骤大致如下:先使用测试将你所需要的场景功能翻译为一个可执行的例子,运行测试,看着他失败,你具有了一个使之通过的清晰目标,只编写足够的生产代码 ---- 不要多写。
    • 测试驱动开发是一个循环的过程。先写失败的测试,是之通过,然后重构代码使得意图更加明确,同时减少重复。每一步过程中,你都不断的运行测试,确保目前的进展。
    • 将场景刻画为可执行的测试是一种设计行为
    • BDD:
      • 通过将有些的 TDD 的实践者的良好习惯正式化,测试驱动开发衍生出了行为驱动开发BDD
      • 人们对 TDD 的误解总是回到测试这个词语,并不是说测试不是TDD 的本质。然而,如果测试不能全面的描述系统的行为,他就会给我们带来一种虚假的安全感,所以我们需要将思考测试转变为思考行为,这种转变如此深刻,开始称 TDD 为 BDD。

单元测试

什么是单元测试

  • 单元测试是针对最小功能单元编写测试代码
  • java 程序的最小功能单元是方法
  • 单元测试是针对单个Java 方法的测试

使用before after 注解

  • Junit 对于每个testcase 方法
    • 先实例化方法对象
    • 执行before方法
    • 执行test方法
    • 执行after方法
  • 这样就使得单个test方法执行前就会创建新的test实例,实例变量的状态不会传递给下一个test方法
  • before方法初始化的对象可以存放在实例字段中,即这个测试类的下面。这样也不会影响其他的test
  • 总结:
    • before 方法一般用于初始化测试对象实例
    • after 方法用于清理before 创建的对象

异常测试

  • 异常有时也是方法的一部分,例如输入电话号码,不符合要求的电话号码应该抛出异常。测试程序需要测试是否可以抛出这种异常。

  • 使用expected 测试异常

    @Test(expected = NumberFormatExpection.clss)
    public void test1 {
    	object.cal("");
    }
    
    if(expresion == null)
    {
    	throw new NumberFormatExpection("Expression is null");
    }
    

参数化测试

超时测试

  • 可以为 JUnit 单个测试设置超时:
  • 超时时间设置为 1 秒:@Test(timeout=1000)

寻求优秀的测试代码

  • 可读的代码才是可以维护的代码

  • 好的结构有助于理解事物

    • 好的代码结构有助于快速可靠的找到高层概念的代码实现
  • 测试的名字一定要正确的表达测试的意图,你必须得信任他

  • 测试必须是独立的可以单独运行的

    要了解测试的独立水平,就观察测试的外部依赖,当看到如下外部依赖时就得特别小心。

    • 时间:你每次执行测试时的系统时间不在你的控制之内
    • 随机数:每次测试生成的随机数无法控制
    • 并发性
    • 基础设施
    • 现存数据
    • 持久化
    • 网络

    以上这些外部依赖你都是无法控制的。

代码坏味道:方法过大;变量类等的命名规范性。

  • 不要让测试类相互依赖
    • 如果测试逻辑包含异步内容或者依赖于当前时间,确保将他们隔离在一个接口之后,这样就可以使用“测试替身”来替换它们从而使测试可重复
  • 测试替身
    • 测试替身是程序员熟知的stub(桩)、fake(伪造对象)、mock(模拟对象)的总称。他们本质上是为了测试目的、用于替换真实协作者的对象。
    • 测试替身促进了许多改进并为我们提供了许多新工具,如:
      • 通过简化要执行的代码来加速执行测试
      • 模拟难以出现的异常情况
      • 观察那些测试代码不可见的状态和交互。
  • 小结:
    • 我们首先指出测试的一个主要优点是可读性,如果难以阅读和理解,测试就会带来维护问题。
    • 接下来我们指出,测试代码的结构有助于使之更好用,允许程序员快速定位到正确的位置,有助于程序员理解发生了什么。
    • 接下来我们阐明,测试有时候是在测试错误的东西
    • 关于测试有时不可靠的问题,我们识别了一些常见原因,以及可重复测试的重要性
    • 最后,提出行业中编写自动化测试的三个基本工具
      • 编写测试的测试框架
      • 运行测试的自动化构建
      • 改善测试以及可测试性的测试替身。

测试替身

  • 使用测试替身的最根本原因,将被测代码与周围隔离开。
  • 我们认为测试替身的作用包括以下几点:
    • 隔离被测代码
    • 加速执行测试:使用真实事物时可能会计算最短路径等,而使用测试替身可以预先设定好返回的路径。
    • 使执行变得确定:如果某些代码天生是不确定的,例如计算最短路径在高峰期或者平时是不一样的。使用测试替身可以分别模拟高峰期和非高峰期两种测试,消除变量,是结果变得确定。
    • 模拟特殊情况:如果汽车通过网络接口google地图去计算最短路径,无法伪造网络连接错误,但是如果使用测试替身的话,则可以在请求连接时抛出一个异常。
    • 访问隐藏信息:例如启动汽车,汽车启动引擎,如何在测试代码中验证引擎是否启动?可以在测试替身中添加仅供测试的方法,避免增加一个永远不会再生产环境中使用的isrunning()方法而弄乱你的生产代码。

测试替身的类型

  • 四种:测试桩、伪造对象、测试间谍和模拟对象。

  • 测试桩通常是短小的。

    • 桩的定义:截断的或非常短的物体

    • 测试桩或stub的目的是用最简单的可能实现来代替真实实现。最基本的实现例子就是一个对象的所有方法都只有一行,且他们各自返回一个适当的默认值。

    • 例如一个日志接口,Logger是为了将日志信息写入日志服务器。

      public class LoggerStub implements Logger {
          public void log(LogLevel level, String message) {
              //no-option
          }
          public LogLevel getLogLevel() {
              return LogLevel.WARN; //hard-corded return value
          }
      }
      
    • 我们有三个充分的理由使用测试桩代替真实的logger实现

      1. 我们的测试不关心被测试代码所写的日志
      2. 我们没有运行日志服务器,所以测试会悲剧的失败
      3. 我们也不希望测试套件在控制台输出大量字节(更别提将所有的数据写入文件了)
    • 有时候,简单的硬编码返回语句和一堆空的 void 方法还不够,有时你至少需要填充一些行为,而有时你需要测试替身根据收到的消息种类来表现出不同的行为。这些情况下,我们应该使用伪造对象。

  • 伪造对象

    • 简称Fake,是一种更加复杂的测试替身,Stub 可以返回硬编码值,而每个测试可能需要有差异的实例化来返回不同的值,以模拟不同的场景。

    • 持续化对象是采用 fake 的典型例子

      假设应用程序架构是这样的:一些***存储对象***提供诗持久化服务,他们知道如何存储和查找指定的对象类型,这种存储对象可能提供的API 如下:

      public interface UserRepository {
          void save(User user);
          User findById(long id);
          User findByName(String userName);
      }
      

      如果没有 伪造对象,测试将全部去访问真实的数据库。你可以使用伪造对象实现一个虚假的数据库,如下:

      public class FakeUserRepository implements UserRepository {
          private Collection<User> users = new ArrayList<User>();  //这模拟了真实数据库中的用户
          public void save(User user){
              //省略
              //每次调用save方法保存用户到数据库时,将数据存储在此集合中,形成一个虚假的数据库,每次调用findById() 方法时,在集合中查找而不用去真实的数据库中查找。
          }
          public findById(long id) {
              for(User user : users) {
                  if(user.getId() == id) return user;
              }
              return null;
          }
      }
      
    • 使用这种另类实现来替换真实事务的优点在于,他比真实的事务要快,她不用每次去查找数据库中的所有数据。

    • 除此之外,我们为了验证代码行为符合预期,在那些情况下,我们可能会求助与测试间谍。

  • 测试间谍偷取秘密

    • public String concat(String first,String second);

      public void filter(List<?> list, Predicate<?> preidate)

    • filter() 方法接收一个列表和一个谓词predicate,过滤列表中不满足谓词的条目。我们无法通过返回值判断此方法的结果,我们验证这个方法正常工作的唯一方法就是事后检查列表。这就像警察卧底,然后汇报它所看到的一切。

    • 使用测试间谍(简称Spy)的方便之处在于,当没有参数作为对象传入时,通过他们的API 也能揭示你想要了解的知识。

      public DLog(DLogTarget… targets); 注意这种传参方法。

    • 测试间谍的工作流程大致如下:

      • 像其他测试替身一样,将他们传入待测试的对象
      • 然后令测试间谍记录已发送的消息。其实就是在spy 中加入一个add方法和列表,将消息记录在列表中。
      • 测试通过断言去询问测试间谍是否已经收到指定消息。
    • 总而言之,测试间谍是一种测试替身,它用于记录过去发生的情况,这样测试在事后就知道所发生的一切。

    • 有时我们进一步利用这个概念,于是测试间谍就变成了全能的模拟对象,如果测试间谍像个卧底警察,那么模拟对象就像渗入暴民的远程控制机器人。

  • 模拟对象

    • 模拟对象(Mock)是特殊的 Spy。
    • 他是一个在特定情境下可配置行为的对象。例如,UserRepository 接口的模拟对象可能被告知:当带着参数123调用findById() 方法时要返回null,而带着参数124调用findById()时要返回User 的一个实例。在这一点上,我们主要讨论的是根据参数来对特定的方法调用打桩。
    • 如果一旦任何意外发生时 Mock 就立即使测试失败,Mock 就能变得更加精确。
    • 包括 JMock、Mockito和 EasyMock 在内的模拟对象库已经是成熟的工具了,崇尚测试的程序员可以借助他们来获取力量。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值