单元测试模拟继承JpaRepository的接口时触发异常问题排查
问题背景
测试PersonServiceImpl的createPerson方法时,执行roleRepository.findById(roleId)返回空,触发EntityNotFoundException异常。尝试过使用Mockito Mock和直接@Autowired注入RoleRepository均未解决,调试发现Mockito生成的RoleRepository实例ID与主类中的不同。
相关代码片段
PersonServiceImpl核心代码
package com.personregistrationspring.service; import com.personregistrationspring.model.Person; import com.personregistrationspring.model.Name; import com.personregistrationspring.model.Address; import com.personregistrationspring.model.Contact; import com.personregistrationspring.model.dto.PersonDto; import com.personregistrationspring.model.Role; import com.personregistrationspring.repository.PersonRepository; import com.personregistrationspring.repository.RoleRepository; import org.springframework.stereotype.Service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import javax.persistence.EntityNotFoundException; import java.util.*; @Service public class PersonServiceImpl implements PersonService{ private final PersonRepository personRepository; private final RoleRepository roleRepository; @Autowired public PersonServiceImpl(PersonRepository personRepository, RoleRepository roleRepository){ this.personRepository = personRepository; this.roleRepository = roleRepository; } @Transactional @Override public PersonDto createPerson(PersonDto personDto){ Person person = mapToEntity(personDto); Set<Integer> roleIds = personDto.getRoleIds(); Set<Role> roles = new HashSet<>(); for(Integer roleId : roleIds){ Optional<Role> optionalRole = roleRepository.findById(roleId); if(optionalRole.isPresent()){ Role role = optionalRole.get(); roles.add(role); } else{ throw new EntityNotFoundException("Role does not exist"); } } person.setRoles(roles); Person createdPerson = personRepository.save(person); return mapToDto(createdPerson); } }
初始测试代码(存在问题)
package com.personregistrationspring; import com.personregistrationspring.model.Role; import com.personregistrationspring.model.RoleEnum; import com.personregistrationspring.repository.PersonRepository; import com.personregistrationspring.repository.RoleRepository; import com.personregistrationspring.model.Person; import com.personregistrationspring.model.dto.PersonDto; import com.personregistrationspring.service.PersonServiceImpl; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.ComponentScan; import org.springframework.test.context.TestPropertySource; import java.util.*; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; @SpringBootTest @ComponentScan(basePackages = "com.personregistrationspring.repository") @TestPropertySource(locations = "classpath:application.properties") public class PersonServiceImplTests { @Mock private PersonRepository personRepository; @Mock private RoleRepository roleRepository; @InjectMocks private PersonServiceImpl personService; @BeforeEach public void setUp(){ MockitoAnnotations.openMocks(this); } @Test public void createPerson_shouldCreatePerson(){ PersonDto personDto = new PersonDto(); Role role = new Role(1, RoleEnum.ADMIN, null); personDto.setRoleIds(Set.of(1)); List<Role> mockRoles = Arrays.asList( new Role(1, RoleEnum.ADMIN, null), new Role(2, RoleEnum.DEV, null), new Role(3, RoleEnum.QA, null) ); when(roleRepository.findAll()).thenReturn(mockRoles); List<Role> roles = roleRepository.findAll(); System.out.println(role.getRoleName()); System.out.println(personDto.getRoleIds()); when(roleRepository.findById(eq(1))).thenReturn(Optional.of(role)); PersonDto createdPersonDto = personService.createPerson(personDto); assertNotNull(createdPersonDto); } }
更新后的测试代码
import com.personregistrationspring.model.Role; import com.personregistrationspring.model.RoleEnum; import com.personregistrationspring.repository.PersonRepository; import com.personregistrationspring.repository.RoleRepository; import com.personregistrationspring.model.dto.PersonDto; import com.personregistrationspring.service.PersonServiceImpl; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.junit.jupiter.api.extension.ExtendWith; import java.util.Optional; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.Mockito.when; @ExtendWith(SpringExtension.class) public class PersonServiceImplTests { @Mock private PersonRepository personRepository; @Mock private RoleRepository roleRepository; @InjectMocks private PersonServiceImpl personService; @BeforeEach public void setUp(){ MockitoAnnotations.openMocks(this); } @Test public void createPerson_shouldCreatePerson(){ PersonDto personDto = new PersonDto(); Role role = new Role(1, RoleEnum.ADMIN, null); personDto.setRoleIds(Set.of(1)); when(roleRepository.findById(1)).thenReturn(Optional.of(role)); PersonDto createdPersonDto = personService.createPerson(personDto); assertNotNull(createdPersonDto); } }
原因分析
初始测试类的混合模式冲突:
初始测试同时使用了@SpringBootTest(启动Spring上下文的集成测试)和@Mock/@InjectMocks(Mockito单元测试),导致Spring上下文和Mockito的Mock实例相互干扰,Mock的RoleRepository没有被正确注入到PersonServiceImpl中,或者Service实际使用的是Spring上下文里的真实Repository而非Mock实例。更新后的测试类缺失关键Mock:
更新后的测试只Mock了roleRepository.findById,但createPerson方法中还调用了personRepository.save(person),如果没有Mock这个方法,它会返回null,导致后续mapToDto(null)可能抛出空指针异常或返回无效对象,同时也会影响测试的完整性。Autowired注入真实Repository的问题:
如果想用真实Repository做集成测试,需要确保测试数据库中存在ID为1的Role数据,否则findById自然返回空。同时集成测试时不能使用@Mock,要通过@Autowired注入真实的PersonService和RoleRepository,避免实例冲突。
修正后的测试代码
方案1:纯Mockito单元测试(推荐,专注业务逻辑)
import com.personregistrationspring.model.Person; import com.personregistrationspring.model.Role; import com.personregistrationspring.model.RoleEnum; import com.personregistrationspring.repository.PersonRepository; import com.personregistrationspring.repository.RoleRepository; import com.personregistrationspring.model.dto.PersonDto; import com.personregistrationspring.service.PersonServiceImpl; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.Optional; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; public class PersonServiceImplTests { @Mock private PersonRepository personRepository; @Mock private RoleRepository roleRepository; @InjectMocks private PersonServiceImpl personService; @BeforeEach public void setUp(){ MockitoAnnotations.openMocks(this); } @Test public void createPerson_shouldCreatePerson(){ // 1. 构造测试数据 PersonDto personDto = new PersonDto(); // 注意:如果mapToEntity需要其他字段,这里要补充设置,比如姓名、地址等 personDto.setRoleIds(Set.of(1)); Role mockRole = new Role(1, RoleEnum.ADMIN, null); Person mockSavedPerson = new Person(); // 给mockSavedPerson设置必要字段,确保mapToDto能正常转换 mockSavedPerson.setRoles(Set.of(mockRole)); // 2. Mock依赖方法 when(roleRepository.findById(1)).thenReturn(Optional.of(mockRole)); when(personRepository.save(any(Person.class))).thenReturn(mockSavedPerson); // 3. 执行测试 PersonDto createdPersonDto = personService.createPerson(personDto); // 4. 断言 assertNotNull(createdPersonDto); // 可以添加更多断言,比如验证角色是否正确设置 } }
方案2:Spring Boot集成测试(使用真实数据库)
import com.personregistrationspring.model.Role; import com.personregistrationspring.model.RoleEnum; import com.personregistrationspring.model.dto.PersonDto; import com.personregistrationspring.service.PersonService; import com.personregistrationspring.repository.RoleRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertNotNull; @SpringBootTest @Transactional // 测试后自动回滚数据 public class PersonServiceImplIntegrationTests { @Autowired private PersonService personService; @Autowired private RoleRepository roleRepository; @BeforeEach public void setUp(){ // 测试前插入必要的角色数据 Role adminRole = new Role(1, RoleEnum.ADMIN, null); roleRepository.save(adminRole); } @Test public void createPerson_shouldCreatePerson(){ PersonDto personDto = new PersonDto(); // 设置PersonDto的所有必填字段 personDto.setRoleIds(Set.of(1)); PersonDto createdPersonDto = personService.createPerson(personDto); assertNotNull(createdPersonDto); } }
内容的提问来源于stack exchange,提问作者Saida

