
在现代应用开发中,逻辑删除(或称软删除)是一种常见的操作,它通过更新记录的某个状态字段(如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<UserEntity, Integer> {
@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)被正确调用,并提升测试的覆盖率?这需要区分两个层面的测试:服务层逻辑的交互验证和存储库层实际操作的覆盖。
在单元测试中,我们通常关注被测试类(如UserService)的业务逻辑是否正确,以及它是否与其依赖(如UserRepository)进行了预期的交互。对于void方法,Mockito.verify()是验证这种交互的关键工具。
目的: 验证UserService是否在特定条件下正确地调用了userRepository的delete方法。
原理: Mockito.verify(mockObject).methodCall(arguments)用于检查模拟对象上的特定方法是否被调用,以及调用次数和参数是否符合预期。
示例代码: 我们将修改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<UserEntity> updatedUserOptional = userRepository.findById(userId);
assertTrue(updatedUserOptional.isPresent(), "用户应该仍然存在于数据库中");
assertTrue(updatedUserOptional.get().isDeleted(), "用户的deleted标志应该被设置为true");
}
}解释: 这个@DataJpaTest测试直接作用于UserRepository,并在一个真实的(尽管是内存中的)数据库环境中执行。当调用userRepository.delete(user)时,@Modifying @Query注解定义的SQL更新语句会被实际执行。随后,通过重新从数据库中查询该用户,并检查其deleted字段,我们能够验证软删除操作是否成功地更新了数据库中的数据。这种测试提供了对存储库层代码的深层次覆盖。
单元测试 vs. 集成测试:
void方法测试:
代码覆盖率:
测试策略:
通过上述方法,您可以确保您的软删除void方法在服务层逻辑和存储库层实现上都得到了充分的验证和覆盖。
以上就是Java单元测试:如何验证和覆盖软删除Void方法的行为的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号