《有效的单元测试》一3.2 测试替身的类型

简介:

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

3.2 测试替身的类型

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

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

3.2.1 测试桩通常是短小的

我这样来定义它:桩(名词),截断的或非常短的物体。
这衍生出测试桩的精确定义。测试桩(简称桩或Stub)的目的是用最简单的可能实现来代替真实实现。最基本的实现例子就是一个对象的所有方法都只有一行,且它们各自返回一个适当的默认值。
假如你负责的代码应当对自己的操作生成一段审计日志,并通过叫做Logger的接口写入远程日志服务器。假如Logger接口仅仅定义了一个方法来产生此类日志,那么Logger接口的桩看起来是这样:
image

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

我们在这个类中硬编码了getLogLevel()方法,它总是返回LogLevel.WARN。有没有搞错?大部分情况下这绝对没问题。毕竟,我们有三个充分的理由来使用测试桩代替真实Logger实现:
1.?我们的测试不关心被测代码所写的日志。
2.?我们没有运行日志服务器,所以测试会悲剧地失败。
3.?我们也不希望测试套件在控制台中输出大量字节(更别提将所有数据写入文件了)。
简而言之,Logger桩实现完美地满足了我们的需要。
有时候,简单的硬编码返回语句和一堆空的void方法还不够。有时候你至少需要填充一些行为,而有时候你需要测试替身根据收到的消息种类来表现出不同的行为。这些情况下,你会借助伪造对象。

3.2.2 伪造对象做事不产生副作用

比起Stub,伪造对象(简称Fake)是一种更加复杂的测试替身。Stub可以返回硬编码值,而每个测试可能需要有差异地实例化来返回不同值,以模拟不同的场景。Fake更像是真实事物的简单版本,优化地伪造真实事物的行为,但是没有副作用或使用真实事物的其他后果。
持久化对象是采用Fake的典型例子。假设应用程序架构是这样的:一些存储对象提供持久化服务,它们知道如何存储和查找指定的对象类型。这种存储对象可能提供的API如下:
image

对于使用存储对象的应用程序,如果没有这种测试替身,测试全都将试图访问真实的数据库。要是对UserRepository接口打桩,令其精确地返回测试所需,你就会感觉好一些。但是模拟更复杂的场景肯定会越发复杂。另一方面,由于UserRepository接口足够简单,以至于你可以实现一个愚蠢而简单的内存数据库,它只提供基本的数据类型。代码清单3.4提供了一个例子。
image

用这种另类实现来替换真实事物的优点在于,它像只鸭子那样嘎嘎叫,还能摇摆,但它摇摆得比真鸭子要快——即使每次查找一个User时都循环一个包含50个条目的列表。
测试桩和伪造对象往往是救命稻草,你可以在测试时用它们替换掉缓慢的真实事物,以及鞭长莫及的依赖。然而,这两种基本的测试替身不总是够用。有时你发现自己面对一堵墙,希望自己能像千里眼一样看透它——为了验证代码行为符合预期。那些情况下,你可能会求助于测试间谍。

3.2.3 测试间谍偷取秘密

你如何测试下列方法?
image

大多数人会说,把这个那个传进去,然后检查返回值是什么的。那可能没问题。毕竟正确的返回值是你最关心的。那么,下列方法又如何测试?
image

这里并没有返回值可以用来断言。这个方法所做的事情是接收一个列表和一个谓词(predicate),过滤列表中不满足谓词的条目。换句话说,验证这个方法正常工作的唯一方式就是事后检查列表。这就像警察卧底,然后汇报她看到的一切。通常你不用测试替身也能做到这一点。这个例子中你可以询问List对象,看它是否包含你所期望的条目。
至于测试替身——我们正在讨论的测试间谍(简称Spy)——的方便之处在于,当没有对象作为参数传入时,通过它们的API也能揭示你想要了解的知识。代码清单3.5显示了这样一个例子。
image

我们先来看看上述代码清单中的场景。被测对象是一个分布式的日志对象DLog,代表了一组DLogTarget。当向DLog写入时,你应该向所有DLogTarget写入相同的消息。从测试的角度来看,事情有点尴尬,你无法知道指定的消息是否被写入,因为DLogTarget接口只定义了一个方法write(),而且DLogTarget、ConsoleTarget和RemoteTarget的真实实现也都没有提供任何方法。
测试间谍登场了。代码清单3.6展示了一个精明的程序员如何鞭打他的女特工去干活。
image
image

这就是测试间谍的一切。像其他测试替身一样,你将它们传入。然后你令测试间谍记录已发送的消息,并让测试询问测试间谍是否收到指定消息。干得漂亮!
简而言之,测试间谍是一种测试替身,它用于记录过去发生的情况,这样测试在事后就知道所发生的一切。有时我们进一步利用这个概念,于是测试间谍就变成了全能的模拟对象。如果测试间谍像个卧底警察,那么模拟对象就像渗入暴民的远程控制机器人。这可能需要一些解释……

3.2.4 模拟对象反对惊喜

模拟对象(简称Mock)是特殊的Spy。它是一个在特定情景下可配置行为的对象。例如,UserRepository接口的模拟对象可能被告之:当带着参数123调用findById()时要返回null,而当带着参数124调用findById()时要返回User的一个实例。在这一点上,我们主要讨论的是根据参数来对特定的方法调用打桩。
如果一旦任何意外发生时Mock就立即使测试失败,Mock就能够变得更加精确。例如,假设我们告诉了模拟对象如何应对带着123或124的findById()调用,它就会严格按照指令工作。对于任何其他的调用——不论是调用不同的方法或者带着另外的参数调用findById()——Mock就会抛出异常,直接使测试失败。同样,如果findById()被调用太多次,Mock就会抱怨——除非我们告诉它允许调用任意次数——如果预期的调用没发生,Mock也会抱怨。
包括JMock、Mockito和EasyMock在内的模拟对象库已经是成熟的工具了,崇尚测试的程序员可以借助它们获得力量。每个库都有自己的行事风格,但基本上你可以用它们中任何一个来完成所有的工作。
这并非模拟对象库的全面教程,但是我们迅速看看代码清单3.7中的例子,它展示了这种库的具体用法。这里我们使用JMock,因为我碰巧有个项目正在使用JMock。
image
image

在这样一小段测试代码中,这个例子展示了许多模拟对象库用法的典型构造。首先,我们告诉库要为指定接口创建一个模拟对象。
在context.checking()中看似笨拙的代码块其实是测试在指导模拟的Internet,告诉它应该期待哪些交互,以及如何应对这些交互。这种情况下,我们预期测试会带着包含"langpair=en%7Cfi"字符串的参数调用get()方法一次,对此,mock应当返回指定字符串。
最终,我们将Mock传给被测的Translator对象,执行Translator,然后断言Translator为我们的场景提供了正确的翻译。
然而,这并非我们的全部断言。如前所述,Mock可以严格地判断已经发生的预期交互。在模拟Internet的例子中,Mock严格地断言它确实收到了一次带有指定子字符串参数的get()方法调用。

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