《有效的单元测试》一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所不能暴露的信息。
现在你理解了使用测试替身的最常见原因。现在该看看不同类型的测试替身了,以及它们各自所具有的优势。

相关文章
|
7天前
|
安全 测试技术 Go
Golang深入浅出之-Go语言单元测试与基准测试:testing包详解
【4月更文挑战第27天】Go语言的`testing`包是单元测试和基准测试的核心,简化了测试流程并鼓励编写高质量测试代码。本文介绍了测试文件命名规范、常用断言方法,以及如何进行基准测试。同时,讨论了测试中常见的问题,如状态干扰、并发同步、依赖外部服务和测试覆盖率低,并提出了相应的避免策略,包括使用`t.Cleanup`、`t.Parallel()`、模拟对象和检查覆盖率。良好的测试实践能提升代码质量和项目稳定性。
14 1
|
7天前
|
监控 JavaScript 前端开发
【TypeScript技术专栏】TypeScript的单元测试与集成测试
【4月更文挑战第30天】本文讨论了在TypeScript项目中实施单元测试和集成测试的重要性。单元测试专注于验证单个函数、类或模块的行为,而集成测试关注不同组件的协作。选用合适的测试框架(如Jest、Mocha),配置测试环境,编写测试用例,并利用模拟和存根进行隔离是关键。集成测试则涉及组件间的交互,需定义测试范围,设置测试数据并解决可能出现的集成问题。将这些测试整合到CI/CD流程中,能确保代码质量和快速响应变化。
|
10天前
|
IDE 测试技术 持续交付
【专栏】Python自动化测试与单元测试框架:提升代码质量与效率
【4月更文挑战第27天】本文探讨了Python自动化测试与单元测试框架在提升代码质量与效率中的作用。Selenium、Appium用于Web和移动应用自动化测试,pytest提供强大、易扩展的测试支持。unittest是Python标准的单元测试框架,支持结构化测试用例和丰富的断言。实践中,应制定测试计划,编写高质量测试用例,实行持续集成与测试,并充分利用测试报告。这些工具和策略能有效保障代码质量和提升开发效率。
|
11天前
|
测试技术 API 持续交付
【专栏】Python自动化测试与单元测试框架
【4月更文挑战第27天】本文探讨了Python在自动化测试与单元测试中的应用,强调其简洁语法和丰富库的优势。文章分为三部分:首先,阐述自动化测试的重要性及Python的易学性、库支持、跨平台和社区支持;其次,介绍了Python的Unittest标准测试框架和Pytest第三方框架的特点与用法;最后,讨论了Web UI和API自动化测试实践,并提出持续集成、测试金字塔等最佳实践。Python为软件开发的测试环节提供了强大支持,帮助构建更稳定的系统。
|
12天前
|
测试技术 数据库 UED
【白盒测试】单元测试的理论基础及用例设计技术(6种)详解
【白盒测试】单元测试的理论基础及用例设计技术(6种)详解
11 1
|
14天前
|
资源调度 JavaScript 测试技术
Vue的集成测试:使用VueTestUtils进行单元测试的技术博文
【4月更文挑战第24天】本文介绍了如何使用VueTestUtils进行Vue.js项目的集成测试。首先,需安装VueTestUtils和vue-template-compiler。接着,展示了如何编写测试用例,包括使用`mount`和`shallowMount`方法挂载组件,以及通过`wrapper`操作和断言组件行为。文章还讨论了单元测试与集成测试的区别,并提到了模拟依赖、交互、组件状态管理和断言的策略。最后,强调了测试的可读性和可维护性对代码质量的重要性。通过VueTestUtils,开发者能更高效地进行Vue组件的测试。
|
15天前
|
资源调度 JavaScript 测试技术
单元测试:编写和运行Vue组件的单元测试
【4月更文挑战第23天】本文探讨了为Vue组件编写单元测试的重要性,以及如何设置测试环境、编写和运行测试。通过使用Jest或Mocha作为测试框架,结合Vue Test Utils,可以独立测试组件的功能,如渲染、事件处理和状态管理。编写测试用例时,应注意覆盖各种行为,并使用断言验证组件状态。运行测试并观察结果,确保测试独立性和高覆盖率。单元测试是保证代码质量和维护性的关键,应随着项目发展持续更新测试用例。
|
Java 测试技术
Java 中的单元测试和集成测试策略
【4月更文挑战第19天】本文探讨了Java开发中的单元测试和集成测试。单元测试专注于单一类或方法的功能验证,使用测试框架如JUnit,强调独立性、高覆盖率和及时更新测试用例。集成测试则验证模块间交互,通过逐步集成或模拟对象来检测系统整体功能。两者相辅相成,确保软件质量和降低修复成本。
|
21天前
|
测试技术 Python
Python 的自动化测试:什么是单元测试和集成测试?在 Python 中如何进行自动化测试?
【4月更文挑战第17天】本文介绍了软件测试中的单元测试和集成测试。单元测试针对单个函数或方法,确保其功能正确;集成测试则检验多个单元交互是否正常。Python 自带的 unittest 模块提供自动化测试框架,示例代码展示了如何创建测试类及测试方法,通过断言验证字符串方法的行为。
11 1
|
23天前
|
测试技术 数据库 开发者
Django自动化测试入门:单元测试与集成测试
【4月更文挑战第15天】本文介绍了Django的自动化测试,包括单元测试和集成测试。单元测试专注于单个视图、模型等组件的正确性,而集成测试则测试组件间的交互。Django测试框架提供`TestCase`和`Client`进行单元和集成测试。通过编写测试,开发者能确保代码质量、稳定性和应用的正确协同工作。运行测试使用`python manage.py test`命令,建议将其纳入日常开发流程。