《有效的单元测试》一3.1 测试替身的威力

《有效的单元测试》一3.1 测试替身的威力

大家好,又见面了,我是全栈君。

本节书摘来自华章出版社《有效的单元测试》一书中的第3章,第3.1节,作者 (芬)Lasse Koskela,更多章节内容可以访问云栖社区“华章计算机”公众号查看

3.1 测试替身的威力

甘地(Mahatma Gandhi)说过:“改变世界从自身做起”。(Be the change you want to see in the world.)测试替身响应了甘地的召唤,成为你在代码中希望见到的变化。牵强附会?容我慢慢道来。
代码是一个大集合。它是指代其他代码的代码网络。每一块都有预定义的行为——作为程序员的你定义了那些行为。某些行为是原子的,包含在单个类或方法中。某些行为意味着不同代码块之间的交互。
为了时不时地验证一段代码的行为符合你的期望,最好的选择是替换其周围的代码,使你获得对环境的完整控制,从而在其中测试你的代码。你有效地将被测代码与其协作者隔离开,以便进行测试,如图3.1所示。

image

这是引入测试替身的最根本原因——将被测代码与周围隔离开。此外,如本章开头所述,还存在许多其他原因。我们认为“仅供测试”的工具是为了:
隔离被测代码
加速执行测试
使执行变得确定
模拟特殊情况
访问隐藏信息
存在多种类型的测试替身可供实现这些效果。多数效果可以用一种测试替身实现,而有些则只匹配于某种特定类型。3.2节会再次讨论这些问题。现在,我想对列出的理由建立共识——在第一时间获得测试替身的理由,以及使用它们的目的。

3.1.1 隔离被测代码

讨论在面向对象编程语言的上下文中隔离被测代码时,我们的世界包含两种东西:
被测代码
与被测代码交互的代码
当我们说要“隔离被测代码”时,意味着将需要测试的代码与所有其他代码隔离开来。如此一来,我们不仅使测试更加有针对性和容易理解,还更容易建立测试。实际上,“所有其他代码”包括了从被测代码中调用的代码。代码清单3.1通过一个简单的例子来展示。
image

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

image

如果我们从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所示。
image

这样的话,如果在不同时间执行测试,你如何确保路径算法的正确性?毕竟,算法肯定是从某个时钟获取了时间,尽管在下午3∶40或3∶50时算法可能建议走高速公路,但如果现在是下午3∶50,那么最佳结果可能突然就变成了走洲际公路,因为高速公路的晚高峰开始了。
测试替身也可以对这类不确定行为伸出援手。例如,当你的骰子变成可以作弊的测试替身,并能产出一串已知的点数序列时,Craps游戏的特定实例突然就变得容易模拟了。相类似,如果你用一个固定时刻的测试替身来替换掉系统时钟,你就更容易去描述某个日志文件的预期输出。
控制你的协作者,并在精确设置被测场景时能够消除所有变量,这是使执行变得确定的关键。说到场景,测试替身也能模拟正常情况下不会发生的情况。

3.1.4 模拟特殊情况

我们编写的大多数软件往往是简单粗暴的——至少在某种意义上,大多数代码都是确定的。因此,通过实例化合适的对象图(object graph),并将其作为参数传入被测代码,我们可以重建几乎任何的情况。当我们从“1 Infinite Loop,Cupertino,CA”出发,设置“1600 Amphitheatre Parkway,Mountain View,CA”为终点,然后说drive()(开车),那么我们可以测试代码清单3.1中Car最终应该停在正确的地方。
我们无法仅用API和产品代码的特性来创建某些情况。假设我们的Route通过互联网从Google地图来获取路线方向。若是请求方向时互联网连接不幸中断,这种情况下该如何测试Route的表现依然正常?
通过禁用计算机的网络接口进行测试,其缺点在于你无法伪造这类网络连接错误,但是若将某处替换为测试替身的话,则可以在请求连接时抛出一个异常。

3.1.5 暴露隐藏的信息

采用测试替身的最后一个(也很重要的)理由,是令我们的测试访问到无法访问的信息。特别是在Java上下文中,“暴露信息”首先想到的是允许测试能够读写其他对象的私有成员。尽管有时你决定去那样做,但这里的信息指的却是被测代码与其协作者之间的交互。
我们再用可靠的Car例子来帮助你掌握这种动态。这是从代码清单3.1中复制的Car类中的代码片段:
image

如你所见,当某人启动汽车Car的时候,汽车Car启动它的引擎Engine。你如何测试它真的发生了?你可以向测试代码暴露私有成员,并为Engine增加一个新方法用于判定引擎是否启动了。但是如果你不想那么做的话呢?要是你不想仅仅为了测试而弄乱生产代码呢?
现在你大概猜到了,答案就是测试替身。通过将Car的Engine替换为测试替身,可以向测试代码中添加仅供测试的方法,避免增加一个永远不会在生产环境中使用的isRunning()方法而弄乱你的生产代码。测试代码如代码清单3.3所示。
image

如你所见,我们的示例测试用测试替身来配置Car,启动汽车,使用测试替身来验证引擎如愿启动。强调一下,isRunning()不是Engine的方法——它是我们添加到TestEngine上的,用于揭示正常Engine所不能暴露的信息。
现在你理解了使用测试替身的最常见原因。现在该看看不同类型的测试替身了,以及它们各自所具有的优势。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/108374.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • 投影矩阵推导_矩阵投影变换

    投影矩阵推导_矩阵投影变换概要投影变换是计算机图形学的基础,理解并推导投影矩阵也是很有必要的。正交投影比较简单,没有透视失真效果(近大远小)。而透视投影比较符合人类的眼睛感知,平行线在远处会相交于一点。投影是通过一个4×4的矩阵来完成的,将视锥映射成标准观察体(齐次裁剪空间)。正交投影OpenGLOpenGL采用的是右手坐标系,z轴朝屏幕向外,因此观察方向是朝着z轴负方向的,那么将x,y,z坐标从区间[l,r],

  • 华为9月3日或推出麒麟9000;TiDB 3.0.18 发布| 极客头条

    华为9月3日或推出麒麟9000;TiDB 3.0.18 发布| 极客头条「极客头条」——技术人员的新闻圈!CSDN的读者朋友们早上好哇,「极客头条」来啦,快来看今天都有哪些值得我们技术人关注的重要新闻吧。国内要闻华为9月3日举行IFA活动:推出麒麟9000,Mate40发布时间或确认华为宣布将于德国当地时间9月3日下午14点(北京时间20点)举办IFA2020主题演讲,预计将会推出5nm麒麟90005G处理器,并且公布Mate40系列发布时间。从目前已有的消息来看,麒麟9000处理器基于台积电5nm工艺打造,并集成华为研发的新NPU、5G基带等技术。(新浪科

  • Android中的layout_gravity和gravity的区别[通俗易懂]

    Android中的layout_gravity和gravity的区别[通俗易懂]在Android的布局中,除了padding和margin容易弄混之外,还有layout_gravity和gravity。按照字面意思来说,layout_gravity就是相对于layout来设置的。

  • Word2vec原理及其Python实现「建议收藏」

    Word2vec原理及其Python实现「建议收藏」目录一、为什么需要WordEmbedding二、Word2vec原理1、CBOW模型2、Skip-gram模型三、行业上已有的预训练词向量四、用Python训练自己的Word2vec词向量一、为什么需要WordEmbedding在NLP(自然语言处理)里面,最细粒度的是词语,词语组成句子,句子再组成段落、篇章、文档。所以要处理NLP的问题,首先就要拿词语开刀…

  • JAVA中的二维数组的定义及使用[通俗易懂]

    JAVA中的二维数组的定义及使用[通俗易懂]二维数组其实是一位数组的嵌套(每一行看做一个内层的一维数组) 两种初始化形式  格式1:动态初始化数据类型数组名[][]=new数据类型[m][n]数据类型[][] 数组名=new数据类型[m][n]数据类型[] 数组名[]=new数据类型[m][n]举例:int[][] arr=new int[5][3]; 也可以理解为“5行3例…

  • talnet服务器搭建

    talnet服务器搭建刚安装的ubuntu12.04还没有telnet功能,需要配置一下我主要是想让ubuntu12.04开启telnet服务做服务器安装openbsd-inetd:#sudoapt-getinstallopenbsd-inetd安装telnetd:#sudoapt-getinstalltelnetd在etc/inetd.conf文件中可以看到这一

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号