首页 > Java > java教程 > 正文

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

心靈之曲
发布: 2025-09-30 13:21:22
原创
308人浏览过

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));
    }
}
登录后复制

注意事项:

白瓜面试
白瓜面试

白瓜面试 - AI面试助手,辅助笔试面试神器

白瓜面试 40
查看详情 白瓜面试
  • @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<UserEntity> 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<UserEntity> 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方法的业务逻辑和底层数据库操作都得到了充分的验证,从而构建出更健壮、更可靠的应用程序。

以上就是Java Spring Boot中软删除Void方法的全面测试策略的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

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