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.hqwc.cn/news/620519.html

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

相关文章

明日周刊-第6期

最近一周杭州的天气起起伏伏&#xff0c;下雨就凉&#xff0c;不下雨就热。但是夏天的感觉确实是越来越浓烈了&#xff0c;又是一年夏&#xff0c;在这个夏天大家都有什么新的计划呢。 文章目录 一周热点资源分享言论歌曲推荐 一周热点 一、我国自主研发科技壮举震惊全球航天界…

电商平台api接口价格接入|定价参考怎么找数据?

目前&#xff0c;不少品牌方都有参考市场中同类商品价格&#xff0c;以辅助自身产品定价的需求。实际上&#xff0c;参考同类产品的定价是一个重要的步骤&#xff0c;能为品牌方带来诸多益处。 首先&#xff0c;通过对同类产品的功能、质量和服务等要素做对比分析&#xff0c;…

(三)C++自制植物大战僵尸游戏项目结构说明

植物大战僵尸游戏开发教程专栏地址http://t.csdnimg.cn/ErelL 一、项目结构 打开项目后&#xff0c;在解决方案管理器中有五个项目&#xff0c;分别是libbox2d、libcocos2d、librecast、libSpine、PlantsVsZombies五个项目&#xff0c;除PlantsVsZombies外&#xff0c;其他四个…

数据结构--选择排序

1、选择排序 1.1 基本认识 1.1.1 基本概念 选择排序是一种简单直观的排序算法&#xff0c;无论什么数据进去都是 O(n) 的时间复杂度。 1.1.2 算法步骤 &#xff08;1&#xff09;首先在未排序序列中找到最小&#xff08;大&#xff09;元素&#xff0c;存放到排序序列的起…

linnux文件服务

1.FTP:文件传输协议。 基础:控制端口(身份验证) command 21/tcp 数据端口: data 20/tcp FTP Server默认配置:yum -y install vsftpd (安装vsftpd&#xff09; touch /var/ftp/abc.txt(创建文件) systemctl start vsftpd(启动文件&#xff09; systemctl …

C# 关于进程回收管理的一款工具设计与分享

目录 设计初衷 开发运行环境 Craneoffice ProcessGC 运行主界面 管理任务与策略 其它设置 移动存储设备管理 核心代码-计时器监控 小结 设计初衷 在使用 COM 模式操作 OFFICE 组件的开发过程中&#xff0c;当操作完相关文档后&#xff0c;在某些情况下仍然无法释放掉…

内网渗透-红队内网渗透工具(Viper)

红队内网渗透工具(Viper) 最近发现一款很强大的内网渗透工具Viper 接下来我给大家介绍一下具体的安装过程&#xff0c;这里我在kali上进行安装 &#xff08;1&#xff09;首先打开kali终端&#xff0c;切换到root用户,确认以下操作都在root用户下操作&#xff0c;sudo -s 安装…

MySQL排序你真的掌握了吗?5个问题考考你

测试sql数据 CREATE TABLE student (id int NOT NULL AUTO_INCREMENT,name varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,age int DEFAULT NULL,money int DEFAULT NULL,PRIMARY KEY (id) USING BTREE,KEY index_name (age) ); INSERT INTO st…

图像分割:Pytorch实现UNet++进行医学细胞分割

图像分割&#xff1a;Pytorch实现UNet进行医学细胞分割 前言相关介绍项目结构具体步骤准备数据集读取数据集设置并解析相关参数定义网络模型定义损失函数定义优化器训练验证 参考 前言 由于本人水平有限&#xff0c;难免出现错漏&#xff0c;敬请批评改正。更多精彩内容&#x…

react项目规范新手教程

简介 React是一种流行的JavaScript库&#xff0c;用于构建用户界面。搭建一个React项目并不难&#xff0c;但确保项目的结构和配置正确可以帮助你更有效地开发和维护应用程序。以下是搭建React项目的一些步骤&#xff1a; 项目规范&#xff1a;项目中有一些开发规范和代码风格…

【计算机毕业设计】4S店车辆管理系统——后附源码

&#x1f389;**欢迎来到我的技术世界&#xff01;**&#x1f389; &#x1f4d8; 博主小档案&#xff1a; 一名来自世界500强的资深程序媛&#xff0c;毕业于国内知名985高校。 &#x1f527; 技术专长&#xff1a; 在深度学习任务中展现出卓越的能力&#xff0c;包括但不限于…

Oracle 11g完全卸载教程(Windows)

文章目录 一、停止Oracle服务二、卸载Oracle1、卸载Oracle产品2、删除注册表3、删除环境变量以及其余文件 一、停止Oracle服务 进入服务 找到服务中的Oracle服务并且停止 全部停止运行成功 二、卸载Oracle 1、卸载Oracle产品 点击开始菜单找到Oracle&#xff0c;然后点击…