0

0

Java Spring Boot中软删除Void方法的全面测试策略

心靈之曲

心靈之曲

发布时间:2025-09-30 13:21:22

|

326人浏览过

|

来源于php中文网

原创

java spring boot中软删除void方法的全面测试策略

针对Spring Boot中执行软删除的void方法,本文阐述了如何通过分层测试实现全面覆盖。对于业务逻辑层,使用Mockito进行单元测试并利用verify验证方法调用;对于数据访问层,则需采用@DataJpaTest进行集成测试,以确保数据库操作的正确性,从而避免因模拟行为而导致的覆盖率缺失。

在开发基于Spring Boot的应用时,我们经常会遇到需要对业务逻辑层(Service)和数据访问层(Repository)进行测试的场景。特别是对于执行“软删除”操作的void方法,其测试覆盖率的实现常常会引发疑问。本文将深入探讨如何为这类方法构建健壮的测试,涵盖单元测试和集成测试两种策略,确保代码的每个环节都得到充分验证。

1. 理解软删除与测试覆盖的挑战

在提供的示例中,userService.deleteUser方法负责业务逻辑,它首先查找用户,然后进行权限检查,最后调用userRepository.delete执行软删除。userRepository.delete方法通过@Modifying和@Query注解,将UserEntity的deleted字段设置为true,而非物理删除。

原始的测试代码deleteUserTest虽然能够验证userService中的异常逻辑,但它并未覆盖到userRepository.delete(userEntity)这一行的实际执行。这主要是因为:

  • 模拟对象行为而非代码执行: 当我们使用Mockito等框架模拟userRepository时,我们只是模拟了它的接口行为(例如,当findById被调用时返回什么),而不是执行userRepository中 @Query 注解所定义的实际SQL更新逻辑。因此,模拟对象的内部实现代码不会被触发,也就不会计入测试覆盖率。
  • 测试范围的界定: 单元测试旨在隔离并验证单个组件的功能。对于userService的单元测试,我们应该模拟其依赖(如userRepository),以确保只测试userService自身的逻辑。如果希望测试userRepository的实际数据库操作,则需要进行集成测试。

为了实现全面的测试覆盖,我们需要采取分层测试的策略。

立即学习Java免费学习笔记(深入)”;

2. 单元测试服务层(Service Layer)

对服务层的单元测试,其核心目标是验证业务逻辑的正确性,包括:

  • 用户查找逻辑。
  • 权限检查(lastAccessDate是否为空)。
  • 在满足条件时,是否正确调用了数据访问层的方法。

在这种情况下,我们应该模拟userRepository,并使用Mockito.verify()来确认userRepository.delete()方法是否被正确调用。

示例代码:改进的userService单元测试

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Date;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository; // 模拟UserRepository

    @InjectMocks
    private UserService userService; // 注入被测试的UserService

    private UserEntity testUser;
    private final String TEST_USER_ID = "1";

    @BeforeEach
    void setUp() {
        testUser = new UserEntity();
        testUser.setId(Integer.valueOf(TEST_USER_ID));
        testUser.setName("Test User");
        // 初始状态,通常为未删除
        testUser.setDeleted(false);
    }

    /**
     * 测试:当用户有效且满足访问策略时,userService应成功调用userRepository的delete方法。
     */
    @Test
    void deleteUser_shouldCallRepositoryDelete_whenValidUserAndAccessDate() {
        // Arrange
        testUser.setLastAccessDate(new Date()); // 设置lastAccessDate以满足策略

        // 模拟userRepository.findById行为
        when(userRepository.findById(Integer.valueOf(TEST_USER_ID)))
                .thenReturn(Optional.of(testUser));

        // Act
        userService.deleteUser(TEST_USER_ID);

        // Assert
        // 验证userRepository.findById是否被调用了一次
        verify(userRepository, times(1)).findById(Integer.valueOf(TEST_USER_ID));
        // 验证userRepository.delete是否被调用了一次,并且传入的是正确的userEntity对象
        verify(userRepository, times(1)).delete(testUser);
    }

    /**
     * 测试:当用户不存在时,userService应抛出UserNotFoundException。
     */
    @Test
    void deleteUser_shouldThrowUserNotFoundException_whenUserNotFound() {
        // Arrange
        when(userRepository.findById(Integer.valueOf(TEST_USER_ID)))
                .thenReturn(Optional.empty()); // 模拟用户不存在

        // Act & Assert
        assertThrows(UserNotFoundException.class, () -> userService.deleteUser(TEST_USER_ID));

        // 验证userRepository.delete方法没有被调用
        verify(userRepository, never()).delete(any(UserEntity.class));
    }

    /**
     * 测试:当用户不满足访问策略(lastAccessDate为null)时,userService应抛出ProhibitedAccessException。
     */
    @Test
    void deleteUser_shouldThrowProhibitedAccessException_whenNoLastAccessDate() {
        // Arrange
        testUser.setLastAccessDate(null); // 设置lastAccessDate为null以违反策略

        when(userRepository.findById(Integer.valueOf(TEST_USER_ID)))
                .thenReturn(Optional.of(testUser));

        // Act & Assert
        assertThrows(ProhibitedAccessException.class, () -> userService.deleteUser(TEST_USER_ID));

        // 验证userRepository.delete方法没有被调用
        verify(userRepository, never()).delete(any(UserEntity.class));
    }
}

注意事项:

Voicenotes
Voicenotes

Voicenotes是一款简单直观的多功能AI语音笔记工具

下载
  • @ExtendWith(MockitoExtension.class):启用Mockito注解。
  • @Mock:创建模拟对象。
  • @InjectMocks:将模拟对象注入到被测试对象中。
  • when(...).thenReturn(...):定义模拟对象的行为。
  • verify(mockObject, times(N)).method(...):验证模拟对象的方法被调用了多少次。
  • verify(mockObject, never()).method(...):验证模拟对象的方法从未被调用。
  • any(UserEntity.class):匹配任何UserEntity类型的参数。

3. 集成测试数据访问层(Repository Layer)

服务层的单元测试验证了业务逻辑和方法调用,但它没有验证userRepository.delete中@Query注解定义的SQL语句是否正确执行并更新了数据库。为了验证这一点,我们需要编写集成测试。

集成测试会启动一个部分或完整的Spring上下文,并与真实的(通常是内存中的)数据库进行交互。

示例代码:userRepository集成测试

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;

import java.util.Date;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;

// @DataJpaTest注解用于测试JPA组件。它会配置一个in-memory数据库(如H2)
// 并自动扫描@Entity和Spring Data JPA仓库。
@DataJpaTest
class UserRepositoryIntegrationTest {

    @Autowired
    private UserRepository userRepository; // 注入真实的UserRepository

    @Autowired
    private TestEntityManager entityManager; // 用于在测试中管理实体(持久化、查找、刷新等)

    /**
     * 测试:验证userRepository的delete方法是否正确执行软删除操作。
     */
    @Test
    void delete_shouldSoftDeleteUser() {
        // Arrange
        // 创建一个UserEntity实例并设置初始状态
        UserEntity user = new UserEntity();
        user.setName("Integration Test User");
        user.setDeleted(false); // 初始状态为未删除
        user.setLastAccessDate(new Date()); // 设置一个日期,虽然对于仓库测试不严格需要,但保持一致性

        // 使用TestEntityManager持久化用户到in-memory数据库
        UserEntity persistedUser = entityManager.persistAndFlush(user);
        // 清除EntityManager缓存,确保后续从数据库中获取的是最新状态
        entityManager.clear();

        // Act
        // 从数据库中重新获取用户,确保操作的是一个受管理的实体
        Optional userOptional = userRepository.findById(persistedUser.getId());
        assertThat(userOptional).isPresent();
        UserEntity userToDelete = userOptional.get();

        // 调用userRepository的delete方法执行软删除
        userRepository.delete(userToDelete);
        // 刷新EntityManager,确保SQL语句被执行到数据库
        entityManager.flush();

        // Assert
        // 再次从数据库中查找用户,验证其deleted字段是否已更新为true
        UserEntity softDeletedUser = entityManager.find(UserEntity.class, persistedUser.getId());

        assertThat(softDeletedUser).isNotNull();
        assertThat(softDeletedUser.isDeleted()).isTrue(); // 断言软删除成功
        assertThat(softDeletedUser.getName()).isEqualTo("Integration Test User"); // 其他字段应保持不变
    }

    /**
     * 额外测试:验证软删除的用户是否可以被正确查询(如果你的查询排除了软删除用户)。
     * (这取决于你的findById或findAll方法是否考虑deleted字段)
     */
    @Test
    void findById_shouldReturnSoftDeletedUser_whenNotFiltered() {
        // Arrange
        UserEntity user = new UserEntity();
        user.setName("Another Test User");
        user.setDeleted(false);
        user.setLastAccessDate(new Date());
        UserEntity persistedUser = entityManager.persistAndFlush(user);
        entityManager.clear();

        UserEntity userToDelete = userRepository.findById(persistedUser.getId()).orElseThrow();
        userRepository.delete(userToDelete);
        entityManager.flush();
        entityManager.clear();

        // Act
        Optional foundUser = userRepository.findById(persistedUser.getId());

        // Assert
        // 默认的findById不会过滤deleted=true的,所以应该还能找到
        assertThat(foundUser).isPresent();
        assertThat(foundUser.get().isDeleted()).isTrue();
    }
}

注意事项:

  • @DataJpaTest:这是一个特殊的Spring Boot测试注解,它只加载与JPA相关的组件(如DataSource、EntityManager和Spring Data JPA仓库),并默认配置一个嵌入式数据库(如H2),非常适合测试Repository层。
  • TestEntityManager:由@DataJpaTest提供,它是一个用于管理JPA实体的工具,可以在测试中方便地进行持久化、查找和刷新操作,绕过Repository接口直接与数据库交互,以验证Repository方法的实际效果。
  • entityManager.persistAndFlush(user):将实体持久化到数据库并立即同步到数据库,确保数据立即可用。
  • entityManager.clear():清除EntityManager的缓存,确保后续的查找操作是从数据库中加载最新数据,而不是从缓存中获取旧数据。
  • entityManager.find(UserEntity.class, persistedUser.getId()):直接通过EntityManager从数据库中查找实体,用于验证Repository操作后的数据库状态。

4. 总结

为了全面测试Spring Boot中执行软删除的void方法,我们应该采用分层测试的策略:

  1. 单元测试服务层(Service Layer)

    • 目的:验证业务逻辑和方法调用流程。
    • 方法:使用Mockito模拟所有外部依赖(如UserRepository),并通过Mockito.verify()来确认关键方法(如userRepository.delete())是否被正确调用。
    • 覆盖:这会覆盖userService内部的逻辑分支,但不会覆盖userRepository中@Query的实际SQL执行。
  2. 集成测试数据访问层(Repository Layer)

    • 目的:验证数据访问层(UserRepository)的实际数据库操作是否正确,包括@Query注解定义的SQL语句。
    • 方法:使用@DataJpaTest注解启动一个轻量级的Spring上下文,连接到真实的(通常是嵌入式)数据库,并通过TestEntityManager或直接调用UserRepository方法来验证数据库状态。
    • 覆盖:这会覆盖userRepository中@Query定义的SQL语句的执行,确保数据在数据库中的正确变更。

通过结合这两种测试方法,我们可以确保deleteUser方法的业务逻辑和底层数据库操作都得到了充分的验证,从而构建出更健壮、更可靠的应用程序。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
数据分析工具有哪些
数据分析工具有哪些

数据分析工具有Excel、SQL、Python、R、Tableau、Power BI、SAS、SPSS和MATLAB等。详细介绍:1、Excel,具有强大的计算和数据处理功能;2、SQL,可以进行数据查询、过滤、排序、聚合等操作;3、Python,拥有丰富的数据分析库;4、R,拥有丰富的统计分析库和图形库;5、Tableau,提供了直观易用的用户界面等等。

727

2023.10.12

SQL中distinct的用法
SQL中distinct的用法

SQL中distinct的语法是“SELECT DISTINCT column1, column2,...,FROM table_name;”。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

328

2023.10.27

SQL中months_between使用方法
SQL中months_between使用方法

在SQL中,MONTHS_BETWEEN 是一个常见的函数,用于计算两个日期之间的月份差。想了解更多SQL的相关内容,可以阅读本专题下面的文章。

350

2024.02.23

SQL出现5120错误解决方法
SQL出现5120错误解决方法

SQL Server错误5120是由于没有足够的权限来访问或操作指定的数据库或文件引起的。想了解更多sql错误的相关内容,可以阅读本专题下面的文章。

1243

2024.03.06

sql procedure语法错误解决方法
sql procedure语法错误解决方法

sql procedure语法错误解决办法:1、仔细检查错误消息;2、检查语法规则;3、检查括号和引号;4、检查变量和参数;5、检查关键字和函数;6、逐步调试;7、参考文档和示例。想了解更多语法错误的相关内容,可以阅读本专题下面的文章。

360

2024.03.06

oracle数据库运行sql方法
oracle数据库运行sql方法

运行sql步骤包括:打开sql plus工具并连接到数据库。在提示符下输入sql语句。按enter键运行该语句。查看结果,错误消息或退出sql plus。想了解更多oracle数据库的相关内容,可以阅读本专题下面的文章。

821

2024.04.07

sql中where的含义
sql中where的含义

sql中where子句用于从表中过滤数据,它基于指定条件选择特定的行。想了解更多where的相关内容,可以阅读本专题下面的文章。

581

2024.04.29

sql中删除表的语句是什么
sql中删除表的语句是什么

sql中用于删除表的语句是drop table。语法为drop table table_name;该语句将永久删除指定表的表和数据。想了解更多sql的相关内容,可以阅读本专题下面的文章。

423

2024.04.29

俄罗斯Yandex引擎入口
俄罗斯Yandex引擎入口

2026年俄罗斯Yandex搜索引擎最新入口汇总,涵盖免登录、多语言支持、无广告视频播放及本地化服务等核心功能。阅读专题下面的文章了解更多详细内容。

158

2026.01.28

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 3万人学习

C# 教程
C# 教程

共94课时 | 7.8万人学习

Java 教程
Java 教程

共578课时 | 52.6万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号