Java应用Service层测试报错求助:Mock静态Mapper失败及级联测试疑问
Java Service层测试问题解决指南
一、静态Mapper方法Mock报错分析与解决
报错原因(翻译后报错信息)
org.mockito.exceptions.misusing.MissingMethodInvocationException: when() 需要一个参数,该参数必须是“对mock对象的方法调用”。 例如: when(mock.getArticles()).thenReturn(articles); 此外,该错误可能出现的原因: 1. 你尝试Stub以下类型的方法:final/private/equals()/hashCode()方法。 这些方法**无法**被Stub/验证。不支持Mock非公开父类中声明的方法。 2. 在when()中你调用的不是mock对象的方法,而是其他对象的方法。
核心问题:PatientMapper.mapToEntity是静态方法,Mockito默认仅支持Mock实例方法,静态方法不属于任何mock对象,因此直接用when()会触发上述异常。
解决方案
方案1:使用Mockito静态Mock(适配Mockito 3.4+版本)
通过MockedStatic类对静态类进行Mock,需放在try-with-resources块中确保测试后清理:
@ExtendWith(MockitoExtension.class) public class PatientServiceTest { @Mock private PatientDao patientDao; @InjectMocks private PatientServiceImpl patientService; @Test public void testSave() { PatientEntity patient = new PatientEntity(); patient.setId(1L); PatientTO patientTO = new PatientTO(); patientTO.setId(1L); // Mock静态Mapper类 try (MockedStatic<PatientMapper> mockedMapper = Mockito.mockStatic(PatientMapper.class)) { mockedMapper.when(() -> PatientMapper.mapToEntity(patientTO)).thenReturn(patient); mockedMapper.when(() -> PatientMapper.mapToTO(patient)).thenReturn(patientTO); when(patientDao.save(patient)).thenReturn(patient); PatientTO result = patientService.save(patientTO); assertNotNull(result); verify(patientDao, times(1)).save(patient); // 验证静态方法调用次数 mockedMapper.verify(() -> PatientMapper.mapToEntity(patientTO), times(1)); mockedMapper.verify(() -> PatientMapper.mapToTO(patient), times(1)); } } }
方案2:重构Mapper为非静态类(推荐)
将静态Mapper改为实例类并通过依赖注入使用,更符合Spring设计原则,测试更简洁:
- 修改Mapper类
@Component public class PatientMapper { public PatientTO mapToTO(final PatientEntity patientEntity) { // 原逻辑不变,移除static修饰符 if (patientEntity == null) return null; final PatientTO patientTO = new PatientTO(); patientTO.setId(patientEntity.getId()); patientTO.setFirstName(patientEntity.getFirstName()); patientTO.setLastName(patientEntity.getLastName()); patientTO.setTelephoneNumber(patientEntity.getTelephoneNumber()); patientTO.setEmail(patientEntity.getEmail()); patientTO.setPatientNumber(patientEntity.getPatientNumber()); patientTO.setDateOfBirth(patientEntity.getDateOfBirth()); patientTO.setDoesHaveChronicDiseases(patientEntity.getDoesHaveChronicDisease()); return patientTO; } public PatientEntity mapToEntity(final PatientTO patientTO) { // 原逻辑不变,移除static修饰符 if (patientTO == null) return null; final PatientEntity patientEntity = new PatientEntity(); patientEntity.setId(patientTO.getId()); patientEntity.setFirstName(patientTO.getFirstName()); patientEntity.setLastName(patientTO.getLastName()); patientEntity.setTelephoneNumber(patientTO.getTelephoneNumber()); patientEntity.setEmail(patientTO.getEmail()); patientEntity.setPatientNumber(patientTO.getPatientNumber()); patientEntity.setDateOfBirth(patientTO.getDateOfBirth()); patientEntity.setDoesHaveChronicDisease(patientTO.getDoesHaveChronicDiseases()); return patientEntity; } }
- 修改Service类注入Mapper
@Service @Transactional public class PatientServiceImpl implements PatientService { private final PatientDao patientDao; private final PatientMapper patientMapper; @Autowired public PatientServiceImpl(PatientDao patientDao, PatientMapper patientMapper) { this.patientDao = patientDao; this.patientMapper = patientMapper; } @Override public PatientTO findById(Long ID) { final PatientEntity patientEntity = patientDao.findOne(ID); return patientMapper.mapToTO(patientEntity); } @Override public PatientTO save(PatientTO patientTO) { final PatientEntity patientEntity = patientMapper.mapToEntity(patientTO); patientDao.save(patientEntity); return patientMapper.mapToTO(patientEntity); } }
- 测试类Mock实例Mapper
@ExtendWith(MockitoExtension.class) public class PatientServiceTest { @Mock private PatientDao patientDao; @Mock private PatientMapper patientMapper; @InjectMocks private PatientServiceImpl patientService; @Test public void testSave() { PatientEntity patient = new PatientEntity(); patient.setId(1L); PatientTO patientTO = new PatientTO(); patientTO.setId(1L); when(patientMapper.mapToEntity(patientTO)).thenReturn(patient); when(patientMapper.mapToTO(patient)).thenReturn(patientTO); when(patientDao.save(patient)).thenReturn(patient); PatientTO result = patientService.save(patientTO); assertNotNull(result); verify(patientDao, times(1)).save(patient); verify(patientMapper, times(1)).mapToEntity(patientTO); verify(patientMapper, times(1)).mapToTO(patient); } }
二、验证删除患者时级联删除预约的测试方法
情况1:JPA级联删除(ORM层面自动处理)
如果PatientEntity与AppointmentEntity配置了@OneToMany(cascade = CascadeType.REMOVE),Service只需调用patientDao.delete(),级联由JPA自动完成。
- 单元测试:验证Service调用了删除患者的方法
@Test public void testDeletePatientCascadesAppointments() { Long patientId = 1L; PatientEntity patient = new PatientEntity(); patient.setId(patientId); when(patientDao.findOne(patientId)).thenReturn(patient); patientService.delete(patientId); verify(patientDao, times(1)).delete(patient); }
- 集成测试:验证实际级联效果(用内存数据库如H2)
@SpringBootTest @Transactional public class PatientServiceIntegrationTest { @Autowired private PatientService patientService; @Autowired private PatientDao patientDao; @Autowired private AppointmentDao appointmentDao; @Test public void testDeletePatientCascadesAppointments() { // 准备测试数据 PatientEntity patient = new PatientEntity(); patient.setFirstName("Test"); patientDao.save(patient); AppointmentEntity appointment = new AppointmentEntity(); appointment.setPatient(patient); appointmentDao.save(appointment); // 删除患者 patientService.delete(patient.getId()); // 验证数据被级联删除 assertNull(patientDao.findOne(patient.getId())); assertNull(appointmentDao.findOne(appointment.getId())); } }
情况2:Service层手动处理级联删除
如果Service先删除关联预约再删除患者,测试时需验证两个操作都被执行:
@ExtendWith(MockitoExtension.class) public class PatientServiceTest { @Mock private PatientDao patientDao; @Mock private AppointmentDao appointmentDao; @InjectMocks private PatientServiceImpl patientService; @Test public void testDeletePatientCascadesAppointments() { Long patientId = 1L; patientService.delete(patientId); // 验证先删除关联预约 verify(appointmentDao, times(1)).deleteByPatientId(patientId); // 验证再删除患者 verify(patientDao, times(1)).deleteById(patientId); } }
内容的提问来源于stack exchange,提问作者SlipperyBarrel
相关产品推荐
相关产品推荐

