《有效的单元测试》第三章
第三章 测试替身
本章内容包括:
●我们能用测试替身做些什么
●哪些测试替身可供选择
●使用测试替身的指南
自从我们开始用类和方法来构建软件时,桩( 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 方法还不够。有时候你至少需要填充一些行为,而有时候你需要测试替身根据收到的消息种类来表现出不同的行为。这些情况下,你会借助伪造对象。
如果有对软件测试感兴趣的小伙伴可以加群了解更多:点击进群
《有效的单元测试》第三章相关推荐
- 第三章 UT单元测试——CPU与内存使用率限制
系列文章目录 第一章 UT单元测试--GoogleTest通用构建说明 第二章 UT单元测试--GTest框架实例 第三章 UT单元测试--CPU与内存使用率限制 文章目录 系列文章目录 前言 一.环 ...
- 慕课软件质量保证与测试(第三章.单元测试)
慕课金陵科技学院.软件质量保证与测试.第三章.黑盒测试.单元测试 0 目录 3 黑盒测试 3.9 单元测试 3.9.1课堂重点 3.9.2测试与作业 4 下一章 0 目录 3 黑盒测试 3.9 单元测 ...
- 《构建之法》前三章读后感
通过第一章讲述的概论,理解到软件工程到底是什么,又为何要叫软件工程,他对我们的生活又有什么影响. 通过一些实例我也认识到客户需求分析的重要,就阿超那样的四则运算一样,渐渐的功能和需求就多了. 在第二章 ...
- 走向.NET架构设计—第三章—分层设计,初涉架构(后篇)
走向.NET架构设计-第三章-分层设计,初涉架构(后篇) 前言:本篇主要是接着前两篇文章继续讲述! 本篇的议题如下: 4. 数据访问层设计 5. 显示层设计 6. UI层设计 4. 数据访问层设 ...
- 软考中项第三章 信息系统集成专业知识
第三章 信息系统集成专业知识 信息系统的生命周期可以分为立项.开发.运维及消亡四个阶段 立项阶段:概念阶段或需求阶段,这一阶段根据用户业务发展和经营管理的需要,提出建设信息系统的初步构想,然后对企业信 ...
- 构建之法前三章读后感—软件工程
本教材不同于其他教材一贯的理知识直接灌溉,而是以对话形式向我们传授知识的,以使我们更好地理解知识点,更加清晰明确. 第一章 第一章的概述中,书本以多种方式,形象生动地向我们阐述了软件工程的内容,也让我 ...
- 关于对《Spring Security3》翻译 (第一章 - 第三章)
原文:http://lengyun3566.iteye.com/category/153689?page=2 翻译说明 最近阅读了<Spring Security3>一书,颇有收获(封面见 ...
- C++ API 设计 08 第三章 模式
第三章 模式 前一章所讨论的品质是用来区分设计良好和糟糕的API.在接下来的几个章节将重点关注构建高品质的API的技术和原则.这个特殊的章节将涵盖一些有用的设计模式和C++ API设计的相关习惯用法. ...
- 敏捷整洁之道 -- 第三章 业务实践
敏捷整洁之道 -- 第三章 业务实践 0. 引子 1. 计划游戏 1.1 三元分析 1.2 故事和点数 1.3 故事 1.4 故事估算 1.5 对迭代进行管理 1.6 速率 2. 小步发布 3. 验收 ...
- 第三章 信息系统集成专业技术知识
第三章 信息系统集成专业技术知识 知识点 1.信息系统的生命周期有哪几个过程 2.信息系统开发的方法有几种:各种用于什么情况的项目. 3.软件需求的定义及分类: 4.软件设计的基本原则是什么: 5.软 ...
最新文章
- Nginx基本配置、性能优化指南
- 你有一张世界互联网大会的门票待领取!数字经济人才专场报名开启
- STM 32如何实现程序自加密
- linux 搭建mysql主从复制 ----第一篇
- dex运行linux,随着三星最新的DeX更新,更多的手机可以使用Linux
- vue过滤器微信小程序过滤器和百度智能小程序过滤器
- C语言中的神兽strdup
- thymeleaf模板引擎使用iframe的解决方案
- 广东省计算机一级网络题分值,计算机一级考试内容题型以及分值
- RocketMQ(四)Linux搭建RocketMQ集群
- Go 日志库 zerolog 大解剖
- excel表格快捷键
- 我所理解的生活(韩寒)
- 女神周迅离婚,Python分析国内离婚情况,结果触目惊心!
- QLabel 添加下划线 删除线
- unity3d内存分析工具memory profiler
- 计算机英语冯敏课后题答案,(中学篇)2020年第10期:例谈基于协同效应的读后续写教学(浙江:冯敏)一文涉及的读后续写试题...
- 小程序实现公农历选择器
- 启用vsftpd日志及其解读
- JS身份证号码校验,JS根据身份证号码获取出生年月日,JS根据出生年月日获取年龄,JS根据身份证号码获取性别
热门文章
- cpu被锁频解除方法_CPU频率被锁定到800mhz怎么办?
- BinaryFormatter serialization and deserialization are disabled within this application
- 提升Hive操作Amazon S3读写数据的性能
- Drupal 建站
- 微博图片设计模板素材推荐 精品 小众
- 百度K站之前兆与解决方案的另类分析
- php FPDF类库应用实现代码
- TZOJ 3244 Happy YuYu's Birthday(数学几何)
- Tool之synergyc:synergyc的简介、安装、使用方法(鼠标键盘控制两台或多台电脑)之详细攻略
- 无线通信中载波带宽是什么?