当前位置: 首页 > news >正文

Spring Start Here 读书笔记:第15 章 Testing your Spring app

测试是一小段逻辑,其目的是验证应用实现的特定功能是否按预期运行。测试分为两类:

  • 单元测试——仅关注独立的逻辑片段
  • 集成测试——专注于验证多个组件之间是否正确交互

测试对于任何应用程序都至关重要。它们确保我们在应用开发过程中所做的更改不会破坏现有功能(或者至少可以降低出错的可能性),并且还能作为文档。许多开发人员忽视测试,因为它们并非应用业务逻辑的直接组成部分,而且编写测试还需要一些时间。
测试的影响通常在短期内难以显现,但测试从长远来看是无价的。无论怎么强调确保正确测试应用逻辑的重要性也不为过。

为什么要编写测试而不是依赖手动测试功能?

  • 因为您可以反复运行测试,以最少的努力验证一切是否按预期运行(持续验证应用程序是否正常运行)
  • 因为通过阅读测试步骤,您可以轻松理解用例的目的(作为文档)
  • 因为测试可以在开发过程中提供有关新应用程序问题的早期反馈

应用的功能最初能够正常工作,后续也可能出错。因为源代码会不断修改(修复错误或添加新功能)。

回归测试是一种不断测试现有功能以验证其是否仍然正常运行的方法。一个好方法是确保针对你实现的任何特定功能测试所有相关场景。这样,你可以在任何更改时运行测试,以验证先前实现的功能是否未受到更改的影响。

如今,我们不再仅仅依赖开发人员手动运行测试,而是将测试执行过程纳入应用的构建流程。通常,开发团队使用我们所谓的持续集成 (CI) 方法:他们会配置一个工具,例如 Jenkins 或 TeamCity,以便在开发人员每次进行更改时运行构建流程。持续集成工具是我们用来执行构建(有时也用于安装)开发过程中实现的应用所需步骤的软件。该 CI 工具还会运行测试,并在出现问题时通知开发人员。

推荐阅读 Cătălin Tudose 的《JUnit in Action》(Manning,2020)。

15.1 编写正确实现的测试

使应用程序可测试和使其可维护(即易于修改以实现新功能和纠正错误)之间存在着密切的联系。可测试性和可维护性是相辅相成的软件品质。

在 Maven 项目中,你可以在项目的project/test文件夹中(代码在project/main文件夹)编写测试类。测试类应该只关注要测试其逻辑的特定方法。即使是简单的逻辑也会产生各种场景。对于每种场景,您都需要在测试类中编写一个方法来验证该特定情况。

测试场景的实现与应用程序的运行方式密切相关,但从技术上讲,任何应用程序的思路都是一样的:确定测试场景,并为每个场景编写一个测试方法。

即使是上一章的转账应用,也至少涉及以下测试场景。

  • 如果无法查到源或目标账户?
  • 如果源账户钱不够,或超过了他的转账限额?
  • 如果一切运行良好?

需要注意的一点是,即使对于一个很小的方法,我们也能找到多个相关的测试场景——这也是保持应用程序中方法简洁的另一个原因!如果你编写了包含大量代码行和参数的大型方法,并且这些方法同时关注多个功能,那么识别相关的测试场景就会变得极其困难。我们认为,如果你无法将不同的职责分离到小巧易读的方法中,那么应用程序的可测试性就会降低。

尽量使用框架也可以保证应用的测试通过性,毕竟框架是专业的人写的,也经过了严格的测试。

15.2 在 Spring 应用中实现测试

以下测试技术是每个开发人员都必须了解的:

  • 编写单元测试来验证方法的逻辑。单元测试简短、执行速度快,并且只关注一个流程。这些测试通过消除所有依赖项,专注于验证一小段逻辑。
  • 编写 Spring 集成测试来验证方法的逻辑及其与框架提供的特定功能的集成。这些测试可以帮助您确保在升级依赖项后,应用的功能仍然有效。

15.2.1 实施单元测试

单元测试是在特定条件下调用特定用例来验证行为的方法。单元测试方法定义用例执行的条件,并验证应用需求所定义的行为。
它们消除了所测试功能的所有依赖关系,仅覆盖特定且独立的逻辑部分。单元测试的目的是验证单个逻辑单元的行为,就像汽车的指示灯一样,它们可以帮助您识别特定组件中的问题(是没油了,还是灯坏了)。

以下均参见示例sq-ch14-ex1


实施第一个单元测试

顺利流程指没有遇到任何错误或异常的执行。通常,顺利流程是最先编写测试的,因为它们是最明显的场景。

任何测试都包含三个主要部分:

  1. 假设:我们需要定义所有输入,并找到我们需要控制的逻辑的依赖关系,以实现所需的流程场景。为此,我们将回答以下问题:我们应该提供哪些输入,以及依赖关系应该如何表现,才能使测试逻辑按照我们期望的特定方式运行?
  2. 调用/执行:我们需要调用测试逻辑来验证其行为。
  3. 验证:我们需要定义需要针对给定逻辑执行的所有验证。

这3个部分中,第一部分最复杂。所有的输入包括方法的参数,还包括方法中引用的但非其创建的对象实例。当涉及到引用对象实例时,会创建模拟对象(mock object)而非使用一个真的对象实例,这样可以让单元测试仅关注一个逻辑,而不会被其引用的对象“牵连”。

TransferServiceUnitTests类包含了所有的测试代码:

public class TransferServiceUnitTests {@Test@DisplayName("Test the amount is transferred from one account to another if no exception occurs.")public void moneyTransferHappyFlow() {AccountRepository accountRepository = mock(AccountRepository.class);TransferService transferService = new TransferService(accountRepository);Account sender = new Account();sender.setId(1);sender.setAmount(new BigDecimal(1000));Account destination = new Account();destination.setId(2);destination.setAmount(new BigDecimal(1000));given(accountRepository.findById(sender.getId())).willReturn(Optional.of(sender));given(accountRepository.findById(destination.getId())).willReturn(Optional.of(destination));transferService.transferMoney(1, 2, new BigDecimal(100));verify(accountRepository).changeAmount(1, new BigDecimal(900));verify(accountRepository).changeAmount(2, new BigDecimal(1100));}
}

其中:

  • @Test注解表示moneyTransferHappyFlow是一个测试方法。
  • @DisplayName注解是对测试场景的描述
  • mock() 方法创建模拟对象,此方法由名为 Mockito 的依赖项提供,通常与 JUnit 一起使用来实现测试。
  • given()方法控制模拟对象的行为。
    -verify() 方法验证模拟对象的方法已被调用。

选中测试类,右键选择Run TransferServiceUnitTests即可运行测试。或者选中项目,右键选择Run Tests in '<project>'。输出在下方左侧。
在这里插入图片描述


为异常流编写测试

除了顺利流程,测试还需涉及异常流程。异常流程测试只许在类中新增一个方法即可。

@ExtendWith(MockitoExtension.class)
public class TransferServiceWithAnnotationsUnitTests {@Mockprivate AccountRepository accountRepository;@InjectMocksprivate TransferService transferService;@Testpublic void moneyTransferDestinationAccountNotFoundFlow() {Account sender = new Account();sender.setId(1);sender.setAmount(new BigDecimal(1000));given(accountRepository.findById(1L)).willReturn(Optional.of(sender));given(accountRepository.findById(2L)).willReturn(Optional.empty());assertThrows(AccountNotFoundException.class,() -> transferService.transferMoney(1, 2, new BigDecimal(100)));verify(accountRepository, never()).changeAmount(anyLong(), any());}
}

注意,在查找接受方时,返回的是Optional.empty()。因此下面用assertThrows()确认抛出了异常。最后,我们使用带有 never() 条件的 verify() 方法来断言 changeAmount() 方法尚未被调用。

这里还增加了3个新的注解,这是作者更推荐的写法:

  • @ExtendWith:启用@Mock 和 @InjectMocks 注释。
  • @Mock:创建一个模拟对象并将其注入到测试类的注解字段中。
  • @InjectMocks:创建测试对象并将其注入到类的注解字段中。

也可以在代码中直接选择需要测试的方法,然后运行。


测试方法返回的值

此处使用的是示例sq-ch9-ex1,测试了登录成功和失败两种情况。

这里主要讲了assertEquals()和verify().addAttribute。略。

15.2.2 实施集成测试

集成测试与单元测试非常相似,仍使用 JUnit 编写。但集成测试关注的不是特定组件的工作方式,而是两个或多个组件如何交互。

编写集成测试针对组件独立正常工作但无法正确通信时问题。此处仍参考示例sq-ch14-ex1

集成测试可以测试应用中两个或多个对象之间的集成;应用程序的对象与框架增强的某些功能的集成;应用与持久化层(数据库)的集成。

集成测试仍遵循相同的步骤:确定假设、调用测试方法并验证结果。但其与单元测试的最大区别是:对于测试涉及的对象,不再使用模拟对象,而是使用真实的对象。当然,对于其他无关的对象,仍可以采用模拟对象。而单元测试必须使用模拟对象。

这里我没有运行成功,因为@Mockbean过时了:

'org.springframework.boot.test.mock.mockito.MockBean' is deprecated since version 3.4.0 and marked for removal

如果您决定不在集成测试中模拟存储库,则应该使用内存数据库(例如 H2)来代替真实数据库。这将帮助您保持测试独立于运行应用的基础架构。使用真实数据库可能会导致测试执行延迟,甚至在基础架构或网络出现问题时导致测试失败。由于您测试的是应用程序而不是基础架构,因此您应该使用模拟内存数据库来避免所有这些麻烦。但是在真正的测试中,还是必须考虑网络中断等情况

在实际应用中,使用单元测试来验证组件的行为,并使用 Spring 集成测试来验证必要的集成场景。不要用Spring 集成测试来验证组件的行为,尽管可以这么做。

总结

  • 测试是一小段代码,用于验证应用中实现的某些逻辑的行为。测试必不可少,因为它们可以帮助您确保未来的应用开发不会破坏现有功能。测试还可以作为文档。
  • 测试分为两类:单元测试和集成测试。
    • 单元测试仅关注孤立的逻辑部分,并验证一个简单组件的运行方式,而不检查它与其他功能的集成情况。单元测试之所以有用,是因为它们执行速度快,并能直接指出特定组件可能面临的问题。
    • 集成测试专注于验证两个或多个组件之间的交互。它们至关重要,因为有时两个组件可能独立运行正常,但无法良好地通信。集成测试可以帮助我们缓解此类情况引发的问题。
  • 有时,在测试中,您希望消除对某些组件的依赖,以便测试能够专注于部分组件(而非所有组件)的交互方式。在这种情况下,我们会将不需要测试的组件替换为“模拟对象”:即您控制的伪造对象,以消除不需要测试的依赖关系,使测试能够专注于特定的交互。
  • 任何测试都包含三个主要部分:
    • 假设:定义输入值和模拟对象的行为方式。
    • 调用/执行:调用要测试的方法。
    • 验证:验证被测试方法的行为方式。
http://www.dtcms.com/a/354407.html

相关文章:

  • 【PyTorch】基于YOLO的多目标检测项目(二)
  • vue2 watch 的使用
  • Xshell 自动化脚本大赛技术文章大纲
  • TypeScript:重载函数
  • 《Linux 网络编程四:TCP 并发服务器:构建模式、原理及关键技术(select )》
  • oceanbase-部署
  • yolo ultralytics之yolov8.yaml文件简介
  • 《信息检索与论文写作》实验报告三 中文期刊文献检索
  • Linux 云服务器内存不足如何优化
  • LinuxC系统多线程程序设计
  • C语言:数据在内存中的存储
  • nginx referer-policy 和 referer
  • redis集群分片策略
  • 【温室气体数据集】NOAA CCGG 飞机观测温室气体
  • 2025年06月 Python(三级)真题解析#中国电子学会#全国青少年软件编程等级考试
  • spring-cloud项目中gateway配置解析
  • DMA学习
  • 【0420】Postgres内核 smgr + md + vfd 实现为指定 table(CREATE TABLE)创建 disk file
  • 每日八股文8.27
  • Linux系统调优工具
  • [Sync_ai_vid] 数据处理流水线 | 配置管理系统
  • 【重学 MySQL】九十二、 MySQL8 密码强度评估与配置指南
  • mysql mvcc机制详解
  • 期权交易中的“道”:从《道德经》中汲取投资智慧
  • RHEL9部署MySQL数据库及数据库的基本使用(增删改查,数据备份恢复)
  • 基于SpringBoot的社区儿童疫苗接种预约系统设计与实现(代码+数据库+LW)
  • Vue将内容生成为二维码,并将所有二维码下载为图片,同时支持批量下载(下载为ZIP),含解决一次性生成过多时页面崩溃解决办法
  • 【雅思020】Opening a bank account
  • C语言二级考试环境配置教程【window篇】
  • 能源行业数据库远程运维安全合规实践:Web化平台的落地经验