Spring Boot 整合 Mockito:提升Java单元测试的高效实践

引言

在Java开发领域,Spring Boot因其便捷的配置和强大的功能而受到广泛欢迎,而Mockito作为一款成熟的单元测试模拟框架,则在提高测试质量、确保代码模块间解耦方面扮演着至关重要的角色。本文将详细介绍如何在Spring Boot项目中整合Mockito,以及Mockito的概念、功能点、优势及实际应用案例。

一、Mockito概念

Mockito是一个面向Java开发者的模拟框架,它的核心目标是**通过创建和配置模拟对象**(Mock Objects)来替代真实依赖项,以便在单元测试中有效地隔离被测代码。在Spring Boot应用程序中,Mockito可用于模拟DAOs、Services、Repositories以及其他依赖服务,使得测试仅针对单一的业务逻辑进行验证,而无需启动数据库、网络请求等实际资源。

为什么写单元测试?

  1. 验证功能正确性
  • 单元测试允许开发者针对代码的最小可测试单元(如类、方法)逐一验证它们是否按预期工作,确保每个独立组件的功能正确无误。
  1. 隔离问题定位
  • 当系统出现问题时,单元测试能快速定位具体哪个模块出现了故障,避免因多个模块相互影响而导致的诊断困难。
  1. 支持持续集成/持续部署(CI/CD)
  • 在CI/CD流水线中,单元测试作为构建过程的一部分,确保每次提交的新代码都不会破坏现有的功能。
  1. 促进重构和演化
  • 编写了充分的单元测试后,重构代码时就有了安全网,可以放心地修改内部结构而不必担心会影响到现有功能。
  1. 设计指导
  • TDD(测试驱动开发)提倡先编写单元测试,这有助于推动设计出更易于测试的代码,即模块化程度更高、依赖关系更清晰的设计。
  1. 文档作用
  • 单元测试实际上是另一种形式的文档,它展示了代码如何被预期使用,以及不同输入下产生的输出,是活生生的、可执行的契约。

单元测试的优点

  1. 尽早发现问题
  • 开发阶段就能发现潜在的缺陷,而不是等到集成测试或生产环境中才显现,节省了后期修正的成本。
  1. 提升代码质量
  • 通过全面覆盖边界条件、异常情况和其他关键场景,促使开发人员考虑更多的边缘用例,从而提高代码的健壮性。
  1. 可维护性
  • 有了良好的单元测试覆盖,未来的开发人员更容易理解代码行为,并有信心在修改代码时不会无意中破坏既有功能。
  1. 依赖管理
  • 使用像Mockito这样的框架可以模拟和隔离依赖项,使得测试关注于单个单元本身的行为,不受外部因素的影响。
  1. 迭代速度
  • 单元测试使得开发周期更快,因为开发人员可以迅速验证他们的更改是否有效,无需每次修改后都进行全面的手动回归测试。
  1. 信心保障
  • 经过单元测试的代码提供了额外的信心,尤其是在大型项目中,确保每个模块的质量,有助于形成稳定的软件整体。

**一种测试手段,更是提升代码质量、支持敏捷开发和维护软件长期稳定性的有效工具。

二、Mockito功能点

  1. Mock对象创建: 使用Mockito的mock()函数可以轻松创建模拟对象,例如,对于一个UserMapper接口:

UserMapper userMapper = Mockito.mock(UserMapper.class);
  1. 方法行为设置: 可以通过when()方法定义模拟对象的方法调用时的预期行为,例如设置返回值或抛出异常:
// 准备测试数据和模拟行为when(userMapper.findUserByUsernameAndPassword(testLoginReq.getUsername(), testLoginReq.getPassword())).thenReturn(null);// 执行测试方法并验证期望的异常被抛出
Exception exception = assertThrows(RuntimeException.class, () -> userService.login(testLoginReq));
  1. 验证方法调用: 使用verify()函数来确保模拟对象的方法已经被正确调用:
// Verify that the method was called with the correct parameters
verify(userMapper).findUserByUsernameAndPassword(testLoginReq.getUsername(), testLoginReq.getPassword());
  1. 参数匹配器: 提供了一系列参数匹配器,如any(), eq(), argThat()等,方便在验证时不需明确指定参数值:
verify(userMapper).findByEmail(argThat(email -> email.endsWith("@example.com")));
  1. Spies: Mockito还支持创建Spy对象,它允许对已有真实对象进行部分模拟,同时保留原有对象的功能:

UserService realUserService = new UserService();
UserServiceImpl userServiceSpy = Mockito.spy(UserServiceImpl);

三、Mockito优势

  • 隔离性:通过模拟依赖项,避免了测试之间不必要的耦合,提高了单元测试的准确性。
  • 简洁性:Mockito API设计简洁明了,使得编写和维护测试代码变得容易。
  • 深度控制:能够精细控制模拟对象的行为,包括方法调用的顺序、次数和异常处理等。
  • 文档作用:通过模拟的交互,反映了被测试代码对外部依赖的使用方式,起到一定的文档作用。

四、Spring Boot整合Mockito案例

添加POM依赖


<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.4.2</version><relativePath/><!-- lookup parent from repository -->
</parent><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope>
</dependency>

业务方法

@Service
@Slf4j(topic = "UserServiceImpl")
public class UserServiceImpl implements UserService {@Resourceprivate UserMapper userMapper;@Overridepublic LoginUserResp login(LoginUserReq loginReq) {log.info("loginReq:{}", loginReq);User user = userMapper.findUserByUsernameAndPassword(loginReq.getUsername(), loginReq.getPassword());if (Objects.isNull(user)) {throw new RuntimeException("用户名或密码错误");}LoginUserResp loginUserResp = new LoginUserResp();loginUserResp.setId(0L);loginUserResp.setUsername(user.getUsername());loginUserResp.setNickName(user.getNickname());loginUserResp.setToken("token");loginUserResp.setPhone("phone");loginUserResp.setUserType(0);return loginUserResp;}@Overridepublic Boolean createUser(UserAddReq userAddReq) {log.info("userAddReq:{}", userAddReq);String email = userAddReq.getEmail();if (Objects.isNull(email)) {throw new RuntimeException("邮箱不能为空");}if (!email.contains("@example.com")) {throw new RuntimeException("邮箱格式不正确");}userMapper.insert(userAddReq);return Boolean.TRUE;}}

UserServiceImplTest 测试类

假设我们正在测试一个UserService类,它依赖于UserMapper。在Spring Boot测试中,可以利用@Mock注解来自动创建并替换Spring容器中的Mock对象:


@ExtendWith(MockitoExtension.class)
public class UserServiceImplTest {@Mockprivate UserMapper userMapper;@InjectMocksprivate UserServiceImpl userService;private User testUser;private LoginUserReq testLoginReq;private LoginUserResp expectedLoginResp;private UserAddReq validUserAddReq;private UserAddReq invalidEmailUserAddReq;private UserAddReq nullEmailUserAddReq;@BeforeEachpublic void setUp() {testUser = new User();testUser.setId(1L);testUser.setUsername("testUser");testUser.setNickname("TestNick");testLoginReq = new LoginUserReq();testLoginReq.setUsername("testUser");testLoginReq.setPassword("password");expectedLoginResp = new LoginUserResp();expectedLoginResp.setId(testUser.getId());expectedLoginResp.setUsername(testUser.getUsername());expectedLoginResp.setNickName(testUser.getNickname());expectedLoginResp.setToken("token");expectedLoginResp.setPhone("phone");expectedLoginResp.setUserType(0);validUserAddReq = new UserAddReq();validUserAddReq.setUsername("testUser");validUserAddReq.setPassword("testPass");validUserAddReq.setEmail("test@example.com");invalidEmailUserAddReq = new UserAddReq();invalidEmailUserAddReq.setUsername("testUser");invalidEmailUserAddReq.setPassword("testPass");invalidEmailUserAddReq.setEmail("test@example");nullEmailUserAddReq = new UserAddReq();nullEmailUserAddReq.setUsername("testUser");nullEmailUserAddReq.setPassword("testPass");nullEmailUserAddReq.setEmail(null);}/*** 测试使用有效的凭据进行登录时,应成功登录。** Arrange 配置测试环境:* 设置当使用测试请求中的用户名和密码调用 userMapper.findUserByUsernameAndPassword 方法时,* 返回预设的测试用户对象。** Act 执行动作:* 使用测试登录请求调用 userService.login 方法,获取实际的登录响应。** Assert 断言结果:* 验证实际的登录响应不为空,并且其各个字段(用户名、昵称、令牌、电话、用户类型)与预期的登录响应相匹配。** Verify 验证调用:* 验证 userMapper.findUserByUsernameAndPassword 方法确实被使用了正确的参数(测试请求中的用户名和密码)调用。*/@Testpublic void whenValidCredentials_thenSuccessfulLogin() {// Arrangewhen(userMapper.findUserByUsernameAndPassword(testLoginReq.getUsername(), testLoginReq.getPassword())).thenReturn(testUser);// ActLoginUserResp actualLoginResp = userService.login(testLoginReq);// AssertassertNotNull(actualLoginResp);assertEquals(expectedLoginResp.getUsername(), actualLoginResp.getUsername());assertEquals(expectedLoginResp.getNickName(), actualLoginResp.getNickName());assertEquals(expectedLoginResp.getToken(), actualLoginResp.getToken());// Verify that the method was called with the correct parametersverify(userMapper).findUserByUsernameAndPassword(testLoginReq.getUsername(), testLoginReq.getPassword());}/*** 测试登录服务时,使用无效的用户名和密码应该导致登录失败。* 这个测试用例验证当提供的用户名和密码不匹配任何已知用户时,login方法是否抛出运行时异常。*/@Testpublic void whenInvalidCredentials_thenLoginFailure() {// 准备测试数据和模拟行为when(userMapper.findUserByUsernameAndPassword(testLoginReq.getUsername(), testLoginReq.getPassword())).thenReturn(null);// 执行测试方法并验证期望的异常被抛出Exception exception = assertThrows(RuntimeException.class, () -> userService.login(testLoginReq));// 验证抛出的异常消息是否匹配预期assertEquals("用户名或密码错误", exception.getMessage());// 验证userMapper的findUserByUsernameAndPassword方法是否被正确调用verify(userMapper).findUserByUsernameAndPassword(testLoginReq.getUsername(), testLoginReq.getPassword());}/*** 测试创建用户功能。* 当提供的用户信息有效时,应该成功保存用户信息并返回true。*/@Testpublic void createUser_WithValidUser_ShouldPersistAndReturnTrue() {// 准备测试环境when(userMapper.insert(any(UserAddReq.class))).thenReturn(1);// 执行测试动作Boolean result = userService.createUser(validUserAddReq);// 验证测试结果assertTrue(result);verify(userMapper).insert(validUserAddReq);}}

五、异常处理与断言
在Mockito中,可以模拟方法抛出异常,并在测试中捕获和验证:

/*** 测试创建用户时使用无效邮箱地址应该抛出异常的情况。* 该测试方法不会返回任何值,它的目的是验证当提供一个无效的邮箱地址时,* {@link userService.createUser(UserAddReq)} 方法是否会抛出预期的 {@link RuntimeException} 异常。* * @param none 该测试方法不接受任何参数。* @return void 该测试方法没有返回值。* @throws RuntimeException 如果提供的用户添加请求中的邮箱地址无效,该方法将抛出异常。*/
@Test
public void createUser_WithInvalidEmail_ShouldThrowException() {// 断言当尝试使用无效的邮箱创建用户时,会抛出运行时异常Exception exception = assertThrows(RuntimeException.class, () -> {userService.createUser(invalidEmailUserAddReq);});// 验证抛出的异常消息是否为预期的错误消息assertEquals("邮箱格式不正确", exception.getMessage());// 验证用户映射器的 insert 方法是否从未被调用verify(userMapper, never()).insert(any(UserAddReq.class));
}/*** 测试创建用户时,如果邮箱为null,应该抛出异常。* 这个测试方法不接受任何参数,也不会返回任何值。* 它主要通过断言验证在尝试使用null邮箱创建用户时,是否会抛出运行时异常,并且异常的消息文本是否正确。*/
@Test
public void createUser_WithNullEmail_ShouldThrowException() {// Act & Assert: 尝试使用null邮箱创建用户,并验证是否抛出了预期的运行时异常Exception exception = assertThrows(RuntimeException.class, () -> {userService.createUser(nullEmailUserAddReq);});assertEquals("邮箱不能为空", exception.getMessage()); // 验证异常消息是否正确verify(userMapper, never()).insert(any(UserAddReq.class)); // 验证用户映射器的insert方法是否从未被调用
}

五、统计单元测试覆盖率

一、单元测试覆盖率概念

单元测试覆盖率是指程序中被执行的单元测试所覆盖的源代码行数或分支数占总行数或分支数的比例。通常分为行覆盖率、分支覆盖率、语句覆盖率、方法覆盖率等多种度量维度。理想的覆盖率并非追求100%,而是力求覆盖所有关键路径和边界条件,以最大程度地暴露潜在错误。

二、单元测试覆盖率的重要性

  1. 保证代码质量:高覆盖率意味着更多的代码逻辑经过了直接或间接的验证,有助于减少因未测试代码引入的缺陷。
  2. 推动重构与优化:覆盖率数据可以帮助识别冗余或难以测试的代码段,进而推动代码结构的改进。
  3. 持续集成与持续部署:在CI/CD流程中,设定合理的覆盖率阈值,可以作为构建是否通过的门槛,防止低质量代码流入生产环境。

三、主流覆盖率统计工具

  1. JaCoCo:JaCoCo是一款适用于Java字节码的开源覆盖率工具,它支持无缝集成到Maven、Gradle构建工具和Eclipse、IntelliJ IDEA等IDE中。对于Spring Boot应用,可以通过JaCoCo插件轻松获取和报告单元测试覆盖率。
<!-- Maven中JaCoCo配置示例 -->
<build><plugins><plugin><groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId><version>0.8.7</version><executions><execution><goals><goal>prepare-agent</goal></goals></execution><execution><id>report</id><phase>test</phase><goals><goal>report</goal></goals></execution></executions></plugin></plugins>
</build>

四、Spring Boot项目中实现覆盖率统计

在Spring Boot项目中,JaCoCo可通过以下步骤实现单元测试覆盖率统计:

  1. 添加JaCoCo相关依赖至构建文件(如上述Maven配置所示)。
  2. 运行单元测试,JaCoCo会在运行时注入代理类收集覆盖率数据。
  3. 测试完成后,JaCoCo会自动生成覆盖率报告,通常位于target/site/jacoco/index.html路径下,打开即可查看详细的覆盖率详情。

此外,在持续集成环境下,可以结合SonarQube等代码质量管理平台,将JaCoCo生成的覆盖率报告导入,实时监控和管理项目的测试覆盖率。

五、本地启用覆盖率

  • 在运行/调试配置对话框中,找到你想要运行的单元测试配置或者创建一个新的JUnit运行配置。
  • 在配置详情页中,找到“Code Coverage”选项卡。

image.png
单元测试报告如下
image.png

六、结论

统计单元测试覆盖率是一项基础且必要的软件工程实践,它能够直观反映测试的质量和全面性。通过合理选择和配置覆盖率工具,配合良好的单元测试策略,开发者能够在不断迭代和演进的软件项目中保持高质量的代码标准,从而降低系统风险,保障产品质量。

六、总结

综上所述,Mockito与Spring Boot的整合为Java开发者提供了一套完整的解决方案,使得单元测试更为精准、高效,从而确保了代码质量、降低了维护成本,并促进了项目的持续集成与交付。通过合理运用Mockito的各项功能,开发者能够编写出高度可信赖且易于维护的单元测试代码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/818875.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

c++总结笔记(一)

计算机可以将程序转化为二进制指令&#xff08;即机器码&#xff09;&#xff0c;并由CPU执行&#xff0c;CPU会按照指令的顺序依次执行每个指令。 C语言特点&#xff1a; 简洁高效可移植模块化标准化 C语言的标准 C89(C90)标准C99标准C11标准 导入 使用include导入包含…

《R语言与农业数据统计分析及建模》学习——数据读入

一、工作目录 # 获取当前工作目录 getwd()# 改变工作目录为指定路径下的文件夹 # 注意工作目录的表达方式 setwd(D:/R_class) setwd(D:\\R_class) 二、文件路径 读取文件中的数据首先要确定文件路径&#xff0c;如果文件不在工作目录下&#xff0c;则必须使用绝对路径 1、文…

Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单实战案例 之十二 简单图片添加水印效果

Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单实战案例 之十二 简单图片添加水印效果 目录 Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单实战案例 之十二 简单图片添加水印效果 一、简单介绍 二、简单图片添加水印效果实现原理 三、简单图片添加水印效果案例…

解决VS2022创建项目只有解决方案看不到项目文件

问题&#xff1a;无法运行、看不到项目文件 解决&#xff1a; 检查环境变量是否正确

开源相机管理库Aravis例程学习(一)——单帧采集single-acquisition

开源相机管理库Aravis例程学习&#xff08;一&#xff09;——单帧采集single-acquisition 简介源码函数说明arv_camera_newarv_camera_acquisitionarv_camera_get_model_namearv_buffer_get_image_widtharv_buffer_get_image_height 简介 本文针对官方例程中的第一个例程&…

MVSplat:稀疏多视点图像的高效3D高斯溅射

MVSplat: Efficient 3D Gaussian Splatting from Sparse Multi-View Images MVSplat&#xff1a;稀疏多视点图像的高效3D高斯溅射 Yuedong Chen1  Haofei Xu2,3  Chuanxia Zheng4  Bohan Zhuang 粤东陈浩飞徐 2,3 郑传霞 4 庄伯涵1 Marc Pollefeys2,5  Andreas Geiger3  T…

长波热红外应用

长波热红外通常是指波长范围在8至14微米之间的红外辐射。这种红外辐射主要来自于物体的热能&#xff0c;因此也称为热红外辐射。相比于短波红外&#xff0c;长波热红外更适合用于测量和探测物体的温度&#xff0c;因为它们能够捕捉到物体辐射的长波长热能&#xff0c;从而提供更…

web自动化系列-selenium find_elements定位方法详解(八)

接上文 &#xff1a; web自动化测试系列-selenium css_selector定位方法详解(七)-CSDN博客 前面已经介绍了8种定位方法 &#xff0c;大多数情况下我们都会优先使用这8种方法 。 但有的时候在你选择定位元素时 &#xff0c;会出现多个同样的定位属性和值 。而且你能选择定位也…

前端框架模板

前端框架模板 1、vue-element-admin vue-element-admin是基于element-ui 的一套后台管理系统集成方案。 **功能&#xff1a;**https://panjiachen.github.io/vue-element-admin-site/zh/guide/#功能 **GitHub地址&#xff1a;**GitHub - PanJiaChen/vue-element-admin: :t…

脚本开发与自动化运维----shell脚本开发及其在DevOps中的应用

一.正则表达式 正则表达式(Regular Expression、regex 或 regexp, 缩写为RE), 又称规则表达式,是计算机科学中的一个概念。正则表通常被用来检索、替换那些符合某个模式(规则)的文本。正则表达式是对字符串(包括普通字符(例如&#xff0c; a 到 z 之间的字母)和特殊字符(称为“…

信也科技网络自动化实践-网络策略管理

1、背景 随着各种法律法规和行业标准的出台和更新&#xff0c;企业或组织需要遵守各种安全合规性要求。网络安全策略管理需要符合这些要求&#xff0c;从而保障企业或组织的安全和合规性。网络安全策略管理需要涵盖企业或组织的整个网络生命周期&#xff0c;包括网络规划、设计…

场景文本检测识别学习 day04(目标检测的基础概念)

经典的目标检测方法 one-stage 单阶段法&#xff1a;YOLO系列、SSD系列 one-stage方法&#xff1a;仅预测一次&#xff0c;直接在特征图上预测每个物体的类别和边界框输入图像之后&#xff0c;使用CNN网络提取特征图&#xff0c;不加入任何补充&#xff08;锚点、锚框&#x…

Kafka 架构深入介绍 及搭建Filebeat+Kafka+ELK

目录 一 架构深入介绍 &#xff08;一&#xff09;Kafka 工作流程及文件存储机制 &#xff08;二&#xff09;数据可靠性保证 &#xff08;三&#xff09;数据一致性问题 &#xff08;四&#xff09;故障问题 &#xff08;五&#xff09;ack 应答机制 二 实…

蓝桥杯2024年第十五届省赛

E:宝石组合 根据给的公式化简后变为gcd(a,b,c)根据算数基本定理&#xff0c;推一下就可以了 然后我们对1到mx的树求约数&#xff0c;并记录约数的次数&#xff0c;我们选择一个最大的且次数大于等3的就是gcd int mx; vector<int> g[N]; vector<int> cnt[N]; int…

基于贝叶斯算法的机器学习在自动驾驶路径规划中的应用实例

目录 第一章 引言 第二章 数据准备 第三章 贝叶斯路径规划模型训练 第四章 路径规划预测 第五章 路径执行 第六章 实验结果分析 第一章 引言 自动驾驶技术的发展带来了自动驾驶车辆的出现&#xff0c;而路径规划作为自动驾驶车辆的关键功能之一&#xff0c;对于确定最佳行…

JVM之JVM栈的详细解析

Java 栈 Java 虚拟机栈&#xff1a;Java Virtual Machine Stacks&#xff0c;每个线程运行时所需要的内存 每个方法被执行时&#xff0c;都会在虚拟机栈中创建一个栈帧 stack frame&#xff08;一个方法一个栈帧&#xff09; Java 虚拟机规范允许 Java 栈的大小是动态的或者是…

标准版uni-app移动端页面添加/开发操作流程

页面简介 uni-app项目中&#xff0c;一个页面就是一个符合Vue SFC规范的.vue文件或.nvue文件。 .vue页面和.nvue页面&#xff0c;均全平台支持&#xff0c;差异在于当uni-app发行到App平台时&#xff0c;.vue文件会使用webview进行渲染&#xff0c;.nvue会使用原生进行渲染。…

用海豚调度器定时调度从Kafka到HDFS的kettle任务脚本

在实际项目中&#xff0c;从Kafka到HDFS的数据是每天自动生成一个文件&#xff0c;按日期区分。而且Kafka在不断生产数据&#xff0c;因此看看kettle是不是需要时刻运行&#xff1f;能不能按照每日自动生成数据文件&#xff1f; 为了测试实际项目中的海豚定时调度从Kafka到HDF…

ActiveMQ主从架构和集群架构的介绍及搭建

一、主从和集群架构的特点 1.1 主从架构的-Master/slave模式特点 读写分离&#xff0c;纵向扩展&#xff0c;所有的写操作一般在master上完成&#xff0c;slave只提供一个热备 1.2 集群架构-Cluster模式特点 分布式的一种存储&#xff0c;水平的扩展&#xff0c;消息的分布…

CCleaner怎么清理软件缓存 CCleaner清理要勾选哪些 ccleanerfree下载

CCleaner软件是一款优秀的数据清理软件&#xff0c;其中没有硬盘和内存的设置&#xff0c;也不含任何广告软件&#xff0c;其出色的注册表清洁功能能够保证您的电脑更稳定运行。本文将围绕CCleaner怎么清理软件缓存&#xff0c;CCleaner清理要勾选哪些的相关内容进行介绍。 一、…