大家好,又见面了,我是全栈君。
本节书摘来自华章出版社《有效的单元测试》一书中的第3章,第3.1节,作者 (芬)Lasse Koskela,更多章节内容可以访问云栖社区“华章计算机”公众号查看
3.1 测试替身的威力
甘地(Mahatma Gandhi)说过:“改变世界从自身做起”。(Be the change you want to see in the world.)测试替身响应了甘地的召唤,成为你在代码中希望见到的变化。牵强附会?容我慢慢道来。
代码是一个大集合。它是指代其他代码的代码网络。每一块都有预定义的行为——作为程序员的你定义了那些行为。某些行为是原子的,包含在单个类或方法中。某些行为意味着不同代码块之间的交互。
为了时不时地验证一段代码的行为符合你的期望,最好的选择是替换其周围的代码,使你获得对环境的完整控制,从而在其中测试你的代码。你有效地将被测代码与其协作者隔离开,以便进行测试,如图3.1所示。
这是引入测试替身的最根本原因——将被测代码与周围隔离开。此外,如本章开头所述,还存在许多其他原因。我们认为“仅供测试”的工具是为了:
隔离被测代码
加速执行测试
使执行变得确定
模拟特殊情况
访问隐藏信息
存在多种类型的测试替身可供实现这些效果。多数效果可以用一种测试替身实现,而有些则只匹配于某种特定类型。3.2节会再次讨论这些问题。现在,我想对列出的理由建立共识——在第一时间获得测试替身的理由,以及使用它们的目的。
3.1.1 隔离被测代码
讨论在面向对象编程语言的上下文中隔离被测代码时,我们的世界包含两种东西:
被测代码
与被测代码交互的代码
当我们说要“隔离被测代码”时,意味着将需要测试的代码与所有其他代码隔离开来。如此一来,我们不仅使测试更加有针对性和容易理解,还更容易建立测试。实际上,“所有其他代码”包括了从被测代码中调用的代码。代码清单3.1通过一个简单的例子来展示。
如你所见,这个例子包含了汽车(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∶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类中的代码片段:
如你所见,当某人启动汽车Car的时候,汽车Car启动它的引擎Engine。你如何测试它真的发生了?你可以向测试代码暴露私有成员,并为Engine增加一个新方法用于判定引擎是否启动了。但是如果你不想那么做的话呢?要是你不想仅仅为了测试而弄乱生产代码呢?
现在你大概猜到了,答案就是测试替身。通过将Car的Engine替换为测试替身,可以向测试代码中添加仅供测试的方法,避免增加一个永远不会在生产环境中使用的isRunning()方法而弄乱你的生产代码。测试代码如代码清单3.3所示。
如你所见,我们的示例测试用测试替身来配置Car,启动汽车,使用测试替身来验证引擎如愿启动。强调一下,isRunning()不是Engine的方法——它是我们添加到TestEngine上的,用于揭示正常Engine所不能暴露的信息。
现在你理解了使用测试替身的最常见原因。现在该看看不同类型的测试替身了,以及它们各自所具有的优势。
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/108374.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...