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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介:

本节书摘来自华章出版社《有效的单元测试》一书中的第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日志并进行多维度分析。
相关文章
|
4月前
|
存储 算法 C语言
用C语言开发游戏的实践过程,包括选择游戏类型、设计游戏框架、实现图形界面、游戏逻辑、调整游戏难度、添加音效音乐、性能优化、测试调试等内容
本文探讨了用C语言开发游戏的实践过程,包括选择游戏类型、设计游戏框架、实现图形界面、游戏逻辑、调整游戏难度、添加音效音乐、性能优化、测试调试等内容,旨在为开发者提供全面的指导和灵感。
124 2
|
4月前
|
测试技术 开发者 UED
探索软件测试的深度:从单元测试到自动化测试
【10月更文挑战第30天】在软件开发的世界中,测试是确保产品质量和用户满意度的关键步骤。本文将深入探讨软件测试的不同层次,从基本的单元测试到复杂的自动化测试,揭示它们如何共同构建一个坚实的质量保证体系。我们将通过实际代码示例,展示如何在开发过程中实施有效的测试策略,以确保软件的稳定性和可靠性。无论你是新手还是经验丰富的开发者,这篇文章都将为你提供宝贵的见解和实用技巧。
|
4月前
|
Web App开发 定位技术 iOS开发
Playwright 是一个强大的工具,用于在各种浏览器上测试应用,并模拟真实设备如手机和平板。通过配置 `playwright.devices`,可以轻松模拟不同设备的用户代理、屏幕尺寸、视口等特性。此外,Playwright 还支持模拟地理位置、区域设置、时区、权限(如通知)和配色方案,使测试更加全面和真实。例如,可以在配置文件中设置全局的区域设置和时区,然后在特定测试中进行覆盖。同时,还可以动态更改地理位置和媒体类型,以适应不同的测试需求。
Playwright 是一个强大的工具,用于在各种浏览器上测试应用,并模拟真实设备如手机和平板。通过配置 `playwright.devices`,可以轻松模拟不同设备的用户代理、屏幕尺寸、视口等特性。此外,Playwright 还支持模拟地理位置、区域设置、时区、权限(如通知)和配色方案,使测试更加全面和真实。例如,可以在配置文件中设置全局的区域设置和时区,然后在特定测试中进行覆盖。同时,还可以动态更改地理位置和媒体类型,以适应不同的测试需求。
360 1
|
6月前
|
IDE 测试技术 持续交付
Python自动化测试与单元测试框架:提升代码质量与效率
【9月更文挑战第3天】随着软件行业的迅速发展,代码质量和开发效率变得至关重要。本文探讨了Python在自动化及单元测试中的应用,介绍了Selenium、Appium、pytest等自动化测试框架,以及Python标准库中的unittest单元测试框架。通过详细阐述各框架的特点与使用方法,本文旨在帮助开发者掌握编写高效测试用例的技巧,提升代码质量与开发效率。同时,文章还提出了制定测试计划、持续集成与测试等实践建议,助力项目成功。
134 5
|
7月前
|
Web App开发 jenkins 测试技术
Selenium 支持的不同测试类型
【8月更文挑战第27天】
74 5
|
7月前
|
测试技术 C# 开发者
“代码守护者:详解WPF开发中的单元测试策略与实践——从选择测试框架到编写模拟对象,全方位保障你的应用程序质量”
【8月更文挑战第31天】单元测试是确保软件质量的关键实践,尤其在复杂的WPF应用中更为重要。通过为每个小模块编写独立测试用例,可以验证代码的功能正确性并在早期发现错误。本文将介绍如何在WPF项目中引入单元测试,并通过具体示例演示其实施过程。首先选择合适的测试框架如NUnit或xUnit.net,并利用Moq模拟框架隔离外部依赖。接着,通过一个简单的WPF应用程序示例,展示如何模拟`IUserRepository`接口并验证`MainViewModel`加载用户数据的正确性。这有助于确保代码质量和未来的重构与扩展。
218 0
|
7月前
|
测试技术 Java Spring
Spring 框架中的测试之道:揭秘单元测试与集成测试的双重保障,你的应用真的安全了吗?
【8月更文挑战第31天】本文以问答形式深入探讨了Spring框架中的测试策略,包括单元测试与集成测试的有效编写方法,及其对提升代码质量和可靠性的重要性。通过具体示例,展示了如何使用`@MockBean`、`@SpringBootTest`等注解来进行服务和控制器的测试,同时介绍了Spring Boot提供的测试工具,如`@DataJpaTest`,以简化数据库测试流程。合理运用这些测试策略和工具,将助力开发者构建更为稳健的软件系统。
98 0
|
7月前
|
测试技术 Java
全面保障Struts 2应用质量:掌握单元测试与集成测试的关键策略
【8月更文挑战第31天】Struts 2 的测试策略结合了单元测试与集成测试。单元测试聚焦于单个组件(如 Action 类)的功能验证,常用 Mockito 模拟依赖项;集成测试则关注组件间的交互,利用 Cactus 等框架确保框架拦截器和 Action 映射等按预期工作。通过确保高测试覆盖率并定期更新测试用例,可以提升应用的整体稳定性和质量。
109 0
|
7月前
|
测试技术 数据库
探索JSF单元测试秘籍!如何让您的应用更稳固、更高效?揭秘成功背后的测试之道!
【8月更文挑战第31天】在 JavaServer Faces(JSF)应用开发中,确保代码质量和可维护性至关重要。本文详细介绍了如何通过单元测试实现这一目标。首先,阐述了单元测试的重要性及其对应用稳定性的影响;其次,提出了提高 JSF 应用可测试性的设计建议,如避免直接访问外部资源和使用依赖注入;最后,通过一个具体的 `UserBean` 示例,展示了如何利用 JUnit 和 Mockito 框架编写有效的单元测试。通过这些方法,不仅能够确保代码质量,还能提高开发效率和降低维护成本。
82 0
|
7月前
|
Java 测试技术 API
SpringBoot单元测试快速写法问题之复杂的业务逻辑设计有效的单元测试如何解决
SpringBoot单元测试快速写法问题之复杂的业务逻辑设计有效的单元测试如何解决

热门文章

最新文章