0

0

Java单元测试:如何验证和覆盖软删除Void方法的行为

聖光之護

聖光之護

发布时间:2025-09-30 14:10:07

|

570人浏览过

|

来源于php中文网

原创

java单元测试:如何验证和覆盖软删除void方法的行为

本文深入探讨了在Java单元测试中,如何有效测试和验证执行软删除操作的void方法。我们将重点关注使用Mockito模拟依赖时,如何验证对存储库delete方法的调用,并区分服务层逻辑测试与存储库实际操作的覆盖,提供相应的代码示例和最佳实践,以确保代码的健壮性和覆盖率。

问题分析:软删除void方法的测试挑战

在现代应用开发中,逻辑删除(或称软删除)是一种常见的操作,它通过更新记录的某个状态字段(如deleted标志)而不是物理删除数据。当我们尝试为包含此类逻辑的void方法编写单元测试时,会遇到一些挑战,尤其是在使用Mocking框架模拟依赖时。

考虑以下用户服务中的deleteUser方法:

public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void deleteUser(String id) {
        var userEntity = userRepository.findById(Integer.valueOf(id))
                .orElseThrow(() -> new UserNotFoundException("Id not found"));
        if (userEntity.getLastAccessDate() == null) {
            throw new ProhibitedAccessException("Policy has been violated");
        }
        // 核心的软删除操作
        userRepository.delete(userEntity);
    }
}

其对应的UserRepository中的delete方法通过JPA的@Modifying @Query注解实现软删除:

public interface UserRepository extends JpaRepository {
    @Modifying
    @Query("update UserEntity u set deleted = true where u = :userEntity")
    void delete(UserEntity userEntity);
}

一个初步的单元测试可能如下所示,它旨在验证在特定条件下会抛出异常:

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

@Test
void deleteUserTest(){
    final int id = 1;
    UserEntity userEntity = new UserEntity();
    // 假设用户未设置LastAccessDate,以便触发ProhibitedAccessException
    userEntity.setLastAccessDate(null); 

    var idString = String.valueOf(id);
    when(userRepository.findById(id)).thenReturn(Optional.of(userEntity));

    // 验证当LastAccessDate为null时,会抛出ProhibitedAccessException
    assertThrows(ProhibitedAccessException.class, () -> userService.deleteUser(idString));
}

这个测试能够成功验证服务层抛出异常的逻辑。然而,它并未覆盖userRepository.delete(userEntity);这一行代码。原因在于,当userEntity.getLastAccessDate()为null时,ProhibitedAccessException被抛出,deleteUser方法提前终止,userRepository.delete()从未被调用。

即使我们修改测试,使其不抛出异常(例如,设置userEntity.setLastAccessDate(LocalDateTime.now())),并期望delete方法被调用,仅仅通过when来模拟findById的行为,并不能直接验证void方法userRepository.delete()是否被调用。这是因为userRepository是一个模拟对象,其delete方法默认不做任何事情,也不会影响测试的覆盖率统计(对于UserRepository类本身而言)。

核心问题是:如何确保userRepository.delete(userEntity)被正确调用,并提升测试的覆盖率?这需要区分两个层面的测试:服务层逻辑的交互验证和存储库层实际操作的覆盖。

解决方案一:使用Mockito验证服务层交互

在单元测试中,我们通常关注被测试类(如UserService)的业务逻辑是否正确,以及它是否与其依赖(如UserRepository)进行了预期的交互。对于void方法,Mockito.verify()是验证这种交互的关键工具

目的: 验证UserService是否在特定条件下正确地调用了userRepository的delete方法。

原理: Mockito.verify(mockObject).methodCall(arguments)用于检查模拟对象上的特定方法是否被调用,以及调用次数和参数是否符合预期。

Skybox AI
Skybox AI

一键将涂鸦转为360°无缝环境贴图的AI神器

下载

示例代码: 我们将修改deleteUserTest,使其能够验证userRepository.delete方法的调用。

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.time.LocalDateTime;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    void deleteUser_shouldCallRepositoryDelete_whenPolicyAllows() {
        // 准备数据
        final int userId = 1;
        UserEntity userEntity = new UserEntity();
        userEntity.setId(userId);
        // 设置LastAccessDate,以允许删除操作通过策略检查
        userEntity.setLastAccessDate(LocalDateTime.now()); 

        String idString = String.valueOf(userId);

        // 模拟findById的行为
        when(userRepository.findById(userId)).thenReturn(Optional.of(userEntity));

        // 执行待测试方法
        userService.deleteUser(idString);

        // 验证:确保findById被调用一次
        verify(userRepository, times(1)).findById(userId);
        // 验证:确保delete方法在UserRepository上被调用一次,并且参数是userEntity
        verify(userRepository, times(1)).delete(userEntity); 
        // 也可以使用any()来匹配任何UserEntity实例,如果不需要精确匹配
        // verify(userRepository, times(1)).delete(any(UserEntity.class)); 
    }

    @Test
    void deleteUser_shouldThrowProhibitedAccessException_whenPolicyViolated() {
        // 准备数据
        final int userId = 2;
        UserEntity userEntity = new UserEntity();
        userEntity.setId(userId);
        // 设置LastAccessDate为null,以触发策略违规异常
        userEntity.setLastAccessDate(null); 

        String idString = String.valueOf(userId);

        // 模拟findById的行为
        when(userRepository.findById(userId)).thenReturn(Optional.of(userEntity));

        // 验证:抛出ProhibitedAccessException
        assertThrows(ProhibitedAccessException.class, () -> userService.deleteUser(idString));

        // 验证:findById被调用一次
        verify(userRepository, times(1)).findById(userId);
        // 验证:delete方法不应该被调用
        verify(userRepository, times(0)).delete(any(UserEntity.class)); 
    }
}

解释: 通过在测试中添加verify(userRepository, times(1)).delete(userEntity);,我们明确地断言了userService.deleteUser方法在执行过程中,确实调用了模拟的userRepository对象的delete方法,并且传入了正确的userEntity实例。这确认了UserService的业务逻辑是正确的,它在满足条件时会尝试执行软删除操作。

然而,需要注意的是,这种方法仅仅验证了“调用行为”。由于userRepository是一个模拟对象,verify操作并没有真正执行UserRepository中@Query定义的SQL更新语句。因此,UserRepository中delete方法的内部逻辑(即update UserEntity u set deleted = true...)的覆盖率并不会因为UserServiceTest而提高。要覆盖这部分代码,我们需要进行更深层次的测试。

解决方案二:测试存储库层以覆盖实际操作

为了覆盖userRepository.delete方法中@Query定义的实际数据库操作,我们需要一个“真实”的存储库实例和数据库环境。这通常通过集成测试或专用的存储库单元测试来实现,利用内存数据库(如H2)或Spring Boot的测试切片功能(如@DataJpaTest)。

目的: 覆盖UserRepository.delete方法中@Modifying @Query注解定义的实际数据库更新逻辑。

原理: 使用@DataJpaTest注解可以为JPA组件提供一个轻量级的测试环境。它会自动配置一个内存数据库(默认为H2),扫描JPA实体和Spring Data JPA存储库,并提供一个事务性的测试上下文。

示例代码:

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.time.LocalDateTime;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertTrue;

// 假设UserEntity和UserRepository已定义
// UserEntity.java
// @Entity
// public class UserEntity {
//     @Id @GeneratedValue private Integer id;
//     private String username;
//     private LocalDateTime lastAccessDate;
//     private boolean deleted; // 新增的软删除标志
//     // getters and setters
// }

@DataJpaTest // 启用JPA测试切片
class UserRepositoryIntegrationTest {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private TestEntityManager entityManager; // 用于直接操作数据库,如插入测试数据

    @Test
    void softDeleteUser_shouldSetDeletedFlagToTrue() {
        // Given: 准备一个未删除的用户实体并持久化到内存数据库
        UserEntity user = new UserEntity();
        user.setUsername("testuser");
        user.setLastAccessDate(LocalDateTime.now());
        user.setDeleted(false); // 初始状态为未删除

        // 使用TestEntityManager持久化,确保数据进入测试数据库
        user = entityManager.persistAndFlush(user); 
        Integer userId = user.getId();

        // When: 调用UserRepository的软删除方法
        userRepository.delete(user); // 这里调用的是带有@Modifying @Query的delete方法

        // 刷新EntityManager,确保事务中的更改被提交或同步到数据库
        entityManager.flush(); 
        entityManager.clear(); // 清除一级缓存,确保从数据库重新加载

        // Then: 验证用户的deleted标志是否已更新为true
        Optional updatedUserOptional = userRepository.findById(userId);

        assertTrue(updatedUserOptional.isPresent(), "用户应该仍然存在于数据库中");
        assertTrue(updatedUserOptional.get().isDeleted(), "用户的deleted标志应该被设置为true");
    }
}

解释: 这个@DataJpaTest测试直接作用于UserRepository,并在一个真实的(尽管是内存中的)数据库环境中执行。当调用userRepository.delete(user)时,@Modifying @Query注解定义的SQL更新语句会被实际执行。随后,通过重新从数据库中查询该用户,并检查其deleted字段,我们能够验证软删除操作是否成功地更新了数据库中的数据。这种测试提供了对存储库层代码的深层次覆盖。

总结与注意事项

  1. 单元测试 vs. 集成测试:

    • 服务层单元测试(使用Mock): 侧重于验证业务逻辑和与依赖的交互。它通过模拟依赖来隔离被测试单元,确保测试的快速性和独立性。对于void方法,主要通过Mockito.verify()来验证交互。
    • 存储库层集成测试(使用真实DB): 侧重于验证数据持久化逻辑(如@Query注解的SQL语句)是否正确地与数据库交互。它需要一个真实的数据库环境,测试速度相对较慢,但提供了更全面的覆盖。
  2. void方法测试:

    • 当void方法是依赖的接口方法时,在单元测试中,我们主要通过Mockito.verify()来验证它是否被调用,以及调用时的参数。我们通常不关心void方法内部的具体实现,因为那是其自身单元测试的职责。
  3. 代码覆盖率:

    • 理解Mock对象的方法调用不会计入被Mock类本身的覆盖率。例如,UserServiceTest中对userRepository.delete()的verify调用,不会增加UserRepository中delete方法的覆盖率。
    • 若要覆盖UserRepository中@Query定义的实际SQL逻辑,必须通过集成测试或专门的存储库单元测试来直接调用并执行该方法。
  4. 测试策略:

    • 结合这两种测试方法是最佳实践。UserServiceTest确保了业务逻辑的正确性,而UserRepositoryIntegrationTest则保证了数据持久化逻辑的可靠性。这种分层测试策略使得问题定位更加精确,并提供了全面的代码验证。

通过上述方法,您可以确保您的软删除void方法在服务层逻辑和存储库层实现上都得到了充分的验证和覆盖。

热门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,提供了直观易用的用户界面等等。

748

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错误的相关内容,可以阅读本专题下面的文章。

1283

2024.03.06

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

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

361

2024.03.06

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

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

861

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

C++ 设计模式与软件架构
C++ 设计模式与软件架构

本专题深入讲解 C++ 中的常见设计模式与架构优化,包括单例模式、工厂模式、观察者模式、策略模式、命令模式等,结合实际案例展示如何在 C++ 项目中应用这些模式提升代码可维护性与扩展性。通过案例分析,帮助开发者掌握 如何运用设计模式构建高质量的软件架构,提升系统的灵活性与可扩展性。

8

2026.01.30

热门下载

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

精品课程

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

共23课时 | 3万人学习

C# 教程
C# 教程

共94课时 | 8万人学习

Java 教程
Java 教程

共578课时 | 53.5万人学习

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

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