有效的单元测试
优秀测试如何提高生产力
- 测试帮助我们捕获错误
- 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实现
- 我们的测试不关心被测试代码所写的日志
- 我们没有运行日志服务器,所以测试会悲剧的失败
- 我们也不希望测试套件在控制台输出大量字节(更别提将所有的数据写入文件了)
-
有时候,简单的硬编码返回语句和一堆空的 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 在内的模拟对象库已经是成熟的工具了,崇尚测试的程序员可以借助他们来获取力量。