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

news2024/11/15 12:51:27

引言

在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 {

    @Resource
    private UserMapper userMapper;


    @Override
    public 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;
    }


    @Override
    public 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 {

    @Mock
    private UserMapper userMapper;

    @InjectMocks
    private UserServiceImpl userService;

    private User testUser;
    private LoginUserReq testLoginReq;
    private LoginUserResp expectedLoginResp;

    private UserAddReq validUserAddReq;
    private UserAddReq invalidEmailUserAddReq;
    private UserAddReq nullEmailUserAddReq;

    @BeforeEach
    public 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 方法确实被使用了正确的参数(测试请求中的用户名和密码)调用。
     */
    @Test
    public void whenValidCredentials_thenSuccessfulLogin() {
        // Arrange
        when(userMapper.findUserByUsernameAndPassword(testLoginReq.getUsername(), testLoginReq.getPassword())).thenReturn(testUser);

        // Act
        LoginUserResp actualLoginResp = userService.login(testLoginReq);

        // Assert
        assertNotNull(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 parameters
        verify(userMapper).findUserByUsernameAndPassword(testLoginReq.getUsername(), testLoginReq.getPassword());
    }


    /**
     * 测试登录服务时,使用无效的用户名和密码应该导致登录失败。
     * 这个测试用例验证当提供的用户名和密码不匹配任何已知用户时,login方法是否抛出运行时异常。
     */
    @Test
    public 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。
     */
    @Test
    public 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.coloradmin.cn/o/1596368.html

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

相关文章

天府锋巢运营方树莓集团——全国园区赋能,助力企业开源节流

树莓集团&#xff0c;作为天府锋巢直播产业基地的运营方&#xff0c;一直在数字产业生态链的建设中走在行业前列。不同于传统的园区或商管公司&#xff0c;树莓集团不仅为企业提供物理入驻空间&#xff0c;更是创造了高效、多维度、多渠道的无边界产业办公体验。这种独特的运营…

第四百五十四回

文章目录 1. 问题描述2. 优化方法2.1 缩小范围2.2 替代方法 3. 示例代码4. 内容总结 我们在上一章回中介绍了"如何获取AppBar的高度"相关的内容&#xff0c;本章回中将介绍关于MediaQuery的优化.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 问题描述 我们在…

package.java文件的作用

你查看springboot的源码&#xff0c;有很多类都有这个文件&#xff0c;在idea不能创建&#xff0c;因为不支持这种命名&#xff0c;只能用记事本创建后复制都项目中。 主要应用是给类添加正常&#xff0c;或者把公用的注解都放到这里&#xff0c;常量不合适&#xff0c;作用范…

爬虫机试题-爬取新闻网站

之前投简历时遇到了这样的一个笔试。本以为会是数据结构算法之类的没想到直接发了一个word直接提需求&#xff0c;感觉挺有意思就写了这篇文章&#xff0c;感兴趣的朋友可以看看。 拿到urllist 通过分析页面结构我们得以知道&#xff0c;这个页面本身没有新闻信息&#xff0c;是…

夜月一帘幽梦,春风十里“三指针法“ (链表面试题篇2)

本篇会加入个人的所谓鱼式疯言 ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. &#x1f92d;&#x1f92d;&#x1f92d;可能说的不是那么严谨.但小编初心是能让更多人能接…

Gemini国内怎么使用

GPT、Claude、Gemini全系列模型国内使用方法来了&#xff01; 一直以来很多人问我能不能有个稳定&#xff0c;不折腾的全球AI大模型测试网站&#xff0c;既能够保证真实靠谱&#xff0c;又能够保证稳定、快速&#xff0c;不要老动不动就挂了、出错或者漫长的响应。 到目前为止…

深度学习入门(3)

一、感知机 感知机接收多个输入信号&#xff0c;输出一个信号。这里所说的“信号”可以想象成电流或河流那样具备“流动性”的东西。 但是&#xff0c;和实际的电 流不同的是&#xff0c;感知机的信号只有“流 / 不流”&#xff08; 1 / 0 &#xff09;两种取值。在本书中&…

FFmpeg: 自实现ijkplayer播放器--03UI界面设计

文章目录 UI设计流程图UI设计界面点击播放功能实现 UI设计流程图 UI设计界面 主界面 控制条 播放列表 画面显示 标题栏 设置界面 提示框 点击播放功能实现 槽函数实现&#xff1a; connect(ui->ctrlBarWind, &CtrlBar::SigPlayOrPause, this, &Main…

vue表格操作列,按钮太多显示... 点击后悬浮显示全部按钮

效果: 分析原理: 一共就三步,仔细看看很简单,位置要加对,代码结构下边有demo 代码结构demo: <el-table-columnlabel"操作"align"center"fixed"right"show-overflow-tooltip><template slot-scope"scope"><el-buttonsi…

绿色地狱steam叫什么 绿色地狱steam怎么搜

绿色地狱steam叫什么 绿色地狱steam怎么搜 《绿色地狱》是一款以亚马逊雨林为背景的开放世界生存模拟游戏。玩家们扮演一名被困在丛林中的冒险者&#xff0c;玩家在游戏内需要学习采集资源、建造庇护所、狩猎和烹饪食物&#xff0c;同时要面对丛林中的危险和挑战&#xff0c;…

Java入门学习Day05

本篇文章主要有创建package、关系运算符、逻辑运算符、三元运算符和其对应的实例。 一、创建package 创建一个包&#xff0c;把我们之前或之后用到的运算符代码都放到这个package里&#xff0c;方面管理。 包的命名通常是公司的网站名称倒着写(com.mayin)&#xff0c;再加上…

简析|抖音隋总分享的人力RPO项目怎么样?

在抖音这个充满活力和创新的平台上&#xff0c;隋总以其独到的见解和深刻的行业洞察&#xff0c;吸引了众多粉丝的关注。最近&#xff0c;隋总分享的人力RPO项目引起了广泛关注&#xff0c;那么&#xff0c;这个项目究竟怎么样呢? 首先&#xff0c;人力RPO&#xff0c;即招聘流…

前端开发攻略---JS将class转换为function。满分写法无死角

1、class写法 class Example {constructor(name) {this.name name}func() {console.log(this.name)}} 2、function写法 use strictfunction Example(name) {if (!new.target) {throw new TypeError(Class constructor Example cannot be invoked without new)}this.name name…

Web 前端 Javascript笔记2

1、数组 为什么需要数组&#xff1a;因为变量只能存储一条数据&#xff0c;但是储存多条数据 数组的声明方式 1、new let a1new Array() console.log(a1) 2、字面量 let a2[1,2,4,"k",true,"zhangsan",11] console.log(a2) 数组里面可以存放不同的数据类…

html--烟花3

html <!DOCTYPE html> <html> <head> <meta charset"UTF-8"> <title>Canvas烟花粒子</title> <meta name"keywords" content"canvas烟花"/> <meta name"description" content"can…

013_NaN_in_Matlab中的非数与调试方法

Matlab中的非数与调试方法 是什么&#xff1f; Matlab编程&#xff08;计算器使用&#xff09;中经常有个错误给你&#xff0c;这句话里可能包含一个关键词NaN。大部分学生都有过被 NaN 支配的痛苦记忆。 NaN 是 Not a Number 的缩写&#xff0c;表示不是一个数字。在 Matla…

00_STM32CubeMX如何新建一个工程

STM32CubeMX如何新建一个工程 STM32CubeMX如何新建一个工程以使用PA1口点亮LED为例子 STM32CubeMX如何新建一个工程 以使用PA1口点亮LED为例子 1.创建一个新工程 2.搜索芯片&#xff0c;然后双击 3.点击PA1引脚&#xff0c;设置为输出口 4.文件一定要保存到英文路径&#xff…

Normalizing Flows

需要学的是神经网络 f f f, 用于完成从source distribution (Pz)&#xff08;latent space&#xff0c;一般为高斯分布&#xff09;到 target distribution (Px) 的映射。 Normalizing Flows 是一种强大的生成模型&#xff0c;它通过学习一个可逆且易于计算的转换来将复杂的概…

Linux多进程开发2 - 进程间通信

1、进程间通信的概念 进程是一个独立的资源分配单元&#xff0c;不同进程之间的资源是独立的&#xff0c;没有关联&#xff0c;不能在一个进程中直接访问另一个进程的资源。但是&#xff0c;进程不是孤立的&#xff0c;不同的进程需要进行信息的交换和状态的传递等&…

【Datawhale LLM学习笔记】一、什么是大型语言模型(LLM)

文章目录 1. 什么是大模型2. 检索增强生成 RAG一、什么是 RAG二、RAG 的工作流程 3. langChain介绍一、什么是 LangChain二、LangChain 的核心组件 4. 开发 LLM 应用的整体流程一、何为大模型开发二、大模型开发的一般流程三、搭建 LLM 项目的流程简析&#xff08;以知识库助手…