第三章 测试替身

本章内容包括:

●我们能用测试替身做些什么

●哪些测试替身可供选择

●使用测试替身的指南

自从我们开始用类和方法来构建软件时,桩( stub)或哑元( dummy)的概念也差不多存在了。过去这类工具主要用于古位,直到真正的事物准备好一一它 允许你在周边代码就位之前就能编译和执行某段代码。

在现代开发者测试的上下文中,这些对象具有了更多的不同目的。除了允许在某些依赖缺失的情况下编译执行代码之外,崇尚测试的程序员还创建了一系列“仅供测试”的工具,用于隔离被测代码、加速执行测试、使随机行为变得确定、模拟特殊情况,以及使测试能够访问隐藏信息。

满足这些目的的各种对象具有相似之处,但又有所区别,我们统称为测试替身(est double)。

我们先探讨开发者采用测试替身的理由。理解了使用测试替身的潜在好处后,我们看看各种可供选择的类型。最后,我们以几个使用测试替身的简单指南来结束本章。

但是现在,我们向问自己,它对我意味着什么?

3.1测试替 身的威力

甘地( Mahatma Gandhi)说过:“改变世界从自身做起”。( Be the change you want to seein the world.) 测试替身响应了甘地的召唤,成为你在代码中希望见到的变化。牵强附会?容我慢慢道来。

代码是一个大集合。它是指代其他代码的代码网络。每一块都有 预定义的行为一作为程序员的你定义了那些行为。某些行为是原子的,包含在单个类或方法中。某些行为意味着不同代码块之间的交互。

为了时不时地验证一段代码的行为符 合你的期望,最好的选择是替换其周围的代码,使你获得对环境的完整控制,从而在其中测试你的代码。你有效地将被测代码与其协作者隔离开,以便进行测试,如图3.1所示。

这是引入测试替身的最根本原因一将 被测代码与周围隔离开。此外,如本章开头所述,还存在许多其他原因。我们认为“仅供测试”的工具是为了:

●隔离被测代码

●加速执行测试

●使执行变得确定

●模拟特殊情况

●访问隐藏信息

存在多种类型的测试替身可供实现这些效果。多数效果可以用一种测试替身实现,而有些则只匹配于某种特定类型。3.2 节会再次讨论这些问题。现在,我想对列出的理由建立共识——在第一时间获得测试替 身的理由,以及使用它们的目的。

3.1.1隔离被测代码

讨论在面向对象编程语言的上下文中隔离被测代码时,我们的世界包含两种东西:

●被测代码

●与被测代码交互的代码

当我们说要“隔离被测代码”时,意味着将需要测试的代码与所有其他代码隔离开来。如此一来,我们不仅使测试更加有针对性和容易理解,还更容易建立测试。实际上,“所有其他代码”包括了从被测代码中调用的代码。代码清单3.1通过一个简单的例子来展示。

代码清单3.1被测代码 (Car)及其协作者( Engine和Route)


public class Car {private Engine engine;public Car (Engine engine) {this. engine = engine;}public void start() {engine.start();}public void drive (Route route) (for (Directions directions : route .directions()) {directions. follow() ;}}public void stop() {engine.stop() ;}
}

如你所见,这个例子包含了汽车(Car)、 汽车引擎(Engine) 和由一系列方向(Directions)组成的路径( Route)。假设现在你想要测试汽车。我们总共有四个类,其中一个是被测代码(Car),两个是协作者( Engine和Route)。为什么Directions不是协作者?某种意义上,Car引用和调用了Directions上的方法。但是还有另一个角度去观察这个场景。我们看看图3.2能否帮助澄清这个观点。

如果我们从Car的方法中引用的类来关注高一级的抽象层次,并站在Car的角度,我们看到的会是Car通过Route来获取和访问Directions(如图3.2)。因此,用测试替身替换Engine和Route,即可将Car与其所有的协作者都隔离开。由于我们用伪实现替换了Route,因此完全控制了向Car提供的各种Directions。

既然你明白了基本原则,即如何通过些测试替 身进行替换从而获得控制,我们再来看看用它们还能做哪些好玩儿的事情。

3.1.2加速执行测试

替换掉真实协作者会带来一个愉悦的副作用, 那就是测试替身的实现经常比真实事物执行得要快。有时,测试替身的速度不只是副作用,而是使用测试替身的主要原因。

考虑图3.2中的驾驶例子。假设初始化Route要涉及加权图搜索算法,以便找出汽车(Car)当前位置与目的地之间的最短路径。由于今日街道和高速公路网络的复杂性,计算需要花一点时间。尽管折腾一次算法可能还比较快,但即使小小的延迟也会积少成多。如果每个测试都初始化一次Route,你可能会在这个算法上消耗好几秒甚至几分钟的CPU周期一当开发者运行自动化测试来获得快速反馈时,几分钟就等于永远。

放置一个测试替身,令它总是返回预先计算好的通往终点的路径,这样就会避免不必要的等待,而且测试运行得更快了。太棒了。但有些地方还是需要那些缓慢的Route算法——在单独有针对性的测试中——但你不希望到处都运行缓慢的算法。

尽管速度总是一件好事,但它不总是最重要的事情。毕竞,如果方向开错了,再快的车也没用。

3.1.3使执行变得确定

我曾听过著名励志演讲家Tony Robbins讲到过惊喜,尽管我们都说自己喜欢惊喜,但我们只喜欢那些自己想要的惊喜。没错,对于软件也样,特别是当谈到测试代码时。用面

测试就是指定行为,并验证行为符合规范。只要代码具有完全确定性,并且其逻辑不包含一丝随机性,这就是简单而直接的。其实,为了使代码(和测试)具有确定性,你就需要能够针对同样的代码重复地运行测试,并总是得到相同的结果。

很多时候,你的生产代码需要包含随机性因素,或者其他因素造成重复执行的结果不唯一。例如,如果你开发一个掷骰子的Craps游戏,你最好让骰子的结果不能预测——这就是随机。

或许不确定行为的最典型情形就是依赖于时间的行为。回到Car的例子,它向Route请求Directions,想象一下用来计算路径的算法会涉及时间,以及流量、限速等,如代码清单3.2所示。

代码清单3.2有时候, 代码行为天生就是不确定的


public class Route {private Clock clock = new Clock() ;private ShortestPath algorithm = new ShortestPath() ;public Collection<Directions> directions() {if (clock. isRushHour()) {return algori thm. avoidBusyIntersections() ;}return algori thm. calculateRouteBetween(...) ;}
}
在高峰时间计算出的路径大不相同!

这样的话,如果在不同时间执行测试,你如何确保路径算法的正确性?毕竟,算法肯定是从某个时钟获取了时间,尽管在下午3: 40或3 : 50时算法可能建议走高速公路,但如果现在是下午3:50,那么最佳结果可能突然就变成了走洲际公路,因为高速公路的晚高峰开始了。

测试替身也可以对这类不确定行为伸出援手。例如,当你的骰子变成可以作弊的测试替身,并能产出一串已知的点数序列时,Craps 游戏的特定实例突然就变得容易模拟了。相类似,如果你用一个固定时刻的测试替身来替换掉系统时钟,你就更容易去描述某个日志文件的预期输出。

控制你的协作者,并在精确设置被测场景时能够消除所有变量,这是使执行变得确定的关键。说到场景,测试替身也能模拟正常情况下不会发生的情况。

3.1.4 模拟特殊情况

我们编写的大多数软件往往是简单粗暴的一至少在某种意义 上,大多数代码都是确定的。因此,通过实例化合适的对象图( object graph),并将其作为参数传入被测代码,我们可以重建几乎任何的情况。当我们从“1 Infinite Loop, Cupertino, CA"出发,设置“1600Amphitheatre Parkway, Mountain View, CA"为终点,然后说drive0 (开车),那么我们可以测试代码清单3.1中Car最终应该停在正确的地方。

我们无法仅用API和产品代码的特性来创建某些情况。假设我们的Route通过互联网从Google地图来获取路线方向。若是请求方向时互联网连接不幸中断,这种情况下该如何测试Route的表现依然正常?

通过禁用计算机的网络接口进行测试,其缺点在于你无法伪造这类网络连接错误,但是若将某处替换为测试替身的话,则可以在请求连接时抛出一个异常。

3.1.5暴露隐藏的信息

采用测试替身的最后一个(也很重要的)理由,是令我们的测试访问到无法访问的信息。特别是在Java上下文中,“暴露信息”首先想到的是允许测试能够读写其他对象的私有成员。尽管有时你决定去那样做,但这里的信息指的却是被测代码与其协作者之间的交互。

我们再用可靠的Car例子来帮助你掌握这种动态。这是从代码清单3.1中复制的Car类中的代码片段:

public class Car {private Engine engine;public void start() {engine.start() ;}// rest omitted for clarity
}

如你所见,当某人启动汽车Car的时候,汽车Car启动它的引擎Engine。你如何测试它真的发生了?你可以向测试代码暴露私有成员,并为Engine增加一个新方法用于判定引擎是否启动了。但是如果你不想那么做的话呢?要是你不想仅仅为了测试而弄乱生产代码呢?

现在你大概猜到了,答案就是测试替身。通过将Car的Engine替换为测试替身,可以向测试代码中添加仅供测试的方法,避免增加一个永远不会在生产环境中使用的isRunning)方法而弄乱你的生产代码。测试代码如代码清单3.3所示。

代码清单3.3测试替身可以提供内幕消息


public class CarTest {@Testpublic void engineIsStartedWhenCarstarts() {TestEngine engine = new TestEngine() ;new Car (engine) .start() ;assertTrue (engine . isRunning());}
}public class TestEngine extends Engine {private boolean isRunning;public void start() isRunning = true;}public boolean isRunning() {return isRunning;}
}➊测试替身来帮忙❷方法仅存在于Tes tEngine,而非Engine

如你所见,我们的示例测试用测试替身➊来配置Car,启动汽车,使用测试替身来验证引擎如愿启动❷。强调一下,isRunning() 不是Engine的方法一它 是我们添加到TestEngine上的,用于揭示正常Engine所不能暴露的信息。

现在你理解了使用测试替身的最常见原因。现在该看看不同类型的测试替身了,以及它们各自所具有的优势。

3.2测试替身的类型

你见过了使用测试替身的各种原因,我们也暗示了有多种测试替身可供选择。我们来仔细看看那些类型吧。图3.3展示了这把大伞下的四种对象。

既然我们已经制定了测试替身的分类,现在就来认识一下它们,并了解相互的区别,以及运用它们的典型目的。我们先从最简单的开始。

3.2.1测试桩通常是短小的

我这样来定义它:桩(名词),截断的或非常短的物体。

这衍生出测试桩的精确定义。测试桩(简称桩或Stub)的目的是用最简单的可能实现来代替真实实现。最基本的实现例子就是一个对象的所有方法都只有 -行,且它们各自返回一个适当的默认值。

假如你负责的代码应当对自己的操作生成-一段审计日志,并通过叫做Logger的接口写人远程日志服务器。假如Logger接口仅仅定义了一个方法来产生此类日志,那么Logger接口的桩看起来是这样:

public class LoggerStub impl ements Logger {public void log (LogLevel level, String message) { }
}

有没有注意到log(方法其实什么都没做?这是桩的典型例子一什么都不做。毕竟,你正是对真实Logger实现打桩,因为你在测试时完全不在乎日志,那么又何必真写日志呢?但是有时候什么都不做也不行。例如,如果Logger接口还定义了一个方法来确定当前设置的日志级别(LogLevel),那么桩实现看起来可能是这样:

public class LoggerStub implements Logger {public void log (LogLevel level, String message) {// still a no-op}public LogLevel getLogLevel() {return LogLevel . WARN; // hard-coded return value}
}

我们在这个类中硬编码了getLogLevel0 方法,它总是返回Log[ evel.WARN。有没有搞错?大部分情况下这绝对没问题。毕竟,我们有三个充分的理由来使用测试桩代替真实Logger实现:

1.我们的测试不关心被测代码所写的日志。

2.我们没有运行日志服务器,所以测试会悲剧地失败。

3.我们也不希望测试套件在控制台中输出大量字节(更别提将所有数据写人文件了)。

简而言之,Logger 桩实现完美地满足了我们的需要。

有时候,简单的硬编码返回语句和一堆空的void 方法还不够。有时候你至少需要填充一些行为,而有时候你需要测试替身根据收到的消息种类来表现出不同的行为。这些情况下,你会借助伪造对象。

如果有对软件测试感兴趣的小伙伴可以加群了解更多:点击进群

《有效的单元测试》第三章相关推荐

  1. 第三章 UT单元测试——CPU与内存使用率限制

    系列文章目录 第一章 UT单元测试--GoogleTest通用构建说明 第二章 UT单元测试--GTest框架实例 第三章 UT单元测试--CPU与内存使用率限制 文章目录 系列文章目录 前言 一.环 ...

  2. 慕课软件质量保证与测试(第三章.单元测试)

    慕课金陵科技学院.软件质量保证与测试.第三章.黑盒测试.单元测试 0 目录 3 黑盒测试 3.9 单元测试 3.9.1课堂重点 3.9.2测试与作业 4 下一章 0 目录 3 黑盒测试 3.9 单元测 ...

  3. 《构建之法》前三章读后感

    通过第一章讲述的概论,理解到软件工程到底是什么,又为何要叫软件工程,他对我们的生活又有什么影响. 通过一些实例我也认识到客户需求分析的重要,就阿超那样的四则运算一样,渐渐的功能和需求就多了. 在第二章 ...

  4. 走向.NET架构设计—第三章—分层设计,初涉架构(后篇)

    走向.NET架构设计-第三章-分层设计,初涉架构(后篇) 前言:本篇主要是接着前两篇文章继续讲述! 本篇的议题如下: 4. 数据访问层设计 5. 显示层设计 6. UI层设计   4.  数据访问层设 ...

  5. 软考中项第三章 信息系统集成专业知识

    第三章 信息系统集成专业知识 信息系统的生命周期可以分为立项.开发.运维及消亡四个阶段 立项阶段:概念阶段或需求阶段,这一阶段根据用户业务发展和经营管理的需要,提出建设信息系统的初步构想,然后对企业信 ...

  6. 构建之法前三章读后感—软件工程

    本教材不同于其他教材一贯的理知识直接灌溉,而是以对话形式向我们传授知识的,以使我们更好地理解知识点,更加清晰明确. 第一章 第一章的概述中,书本以多种方式,形象生动地向我们阐述了软件工程的内容,也让我 ...

  7. 关于对《Spring Security3》翻译 (第一章 - 第三章)

    原文:http://lengyun3566.iteye.com/category/153689?page=2 翻译说明 最近阅读了<Spring Security3>一书,颇有收获(封面见 ...

  8. C++ API 设计 08 第三章 模式

    第三章 模式 前一章所讨论的品质是用来区分设计良好和糟糕的API.在接下来的几个章节将重点关注构建高品质的API的技术和原则.这个特殊的章节将涵盖一些有用的设计模式和C++ API设计的相关习惯用法. ...

  9. 敏捷整洁之道 -- 第三章 业务实践

    敏捷整洁之道 -- 第三章 业务实践 0. 引子 1. 计划游戏 1.1 三元分析 1.2 故事和点数 1.3 故事 1.4 故事估算 1.5 对迭代进行管理 1.6 速率 2. 小步发布 3. 验收 ...

  10. 第三章 信息系统集成专业技术知识

    第三章 信息系统集成专业技术知识 知识点 1.信息系统的生命周期有哪几个过程 2.信息系统开发的方法有几种:各种用于什么情况的项目. 3.软件需求的定义及分类: 4.软件设计的基本原则是什么: 5.软 ...

最新文章

  1. Nginx基本配置、性能优化指南
  2. 你有一张世界互联网大会的门票待领取!数字经济人才专场报名开启
  3. STM 32如何实现程序自加密
  4. linux 搭建mysql主从复制 ----第一篇
  5. dex运行linux,随着三星最新的DeX更新,更多的手机可以使用Linux
  6. vue过滤器微信小程序过滤器和百度智能小程序过滤器
  7. C语言中的神兽strdup
  8. thymeleaf模板引擎使用iframe的解决方案
  9. 广东省计算机一级网络题分值,计算机一级考试内容题型以及分值
  10. RocketMQ(四)Linux搭建RocketMQ集群
  11. Go 日志库 zerolog 大解剖
  12. excel表格快捷键
  13. 我所理解的生活(韩寒)
  14. 女神周迅离婚,Python分析国内离婚情况,结果触目惊心!
  15. QLabel 添加下划线 删除线
  16. unity3d内存分析工具memory profiler
  17. 计算机英语冯敏课后题答案,(中学篇)2020年第10期:例谈基于协同效应的读后续写教学(浙江:冯敏)一文涉及的读后续写试题...
  18. 小程序实现公农历选择器
  19. 启用vsftpd日志及其解读
  20. JS身份证号码校验,JS根据身份证号码获取出生年月日,JS根据出生年月日获取年龄,JS根据身份证号码获取性别

热门文章

  1. cpu被锁频解除方法_CPU频率被锁定到800mhz怎么办?
  2. BinaryFormatter serialization and deserialization are disabled within this application
  3. 提升Hive操作Amazon S3读写数据的性能
  4. Drupal 建站
  5. 微博图片设计模板素材推荐 精品 小众
  6. 百度K站之前兆与解决方案的另类分析
  7. php FPDF类库应用实现代码
  8. TZOJ 3244 Happy YuYu's Birthday(数学几何)
  9. Tool之synergyc:synergyc的简介、安装、使用方法(鼠标键盘控制两台或多台电脑)之详细攻略
  10. 无线通信中载波带宽是什么?