运行Mockito测试类报UnnecessaryStubbingException异常修复方案
问题描述
基于JUnit4+Mockito编写ScreenQueryControllerTest单元测试类时出现异常:单独执行类中任意一个@Test标注的测试方法时无报错,运行整个测试类时,无论类中包含1个还是多个测试方法,都会抛出UnnecessaryStubbingException错误,尝试公开渠道同类问题的常规解决方案均未生效。
对应测试原始代码如下:
package com.services.report.service.controller; import com.services.report.service.model.SmsInfo; import com.services.report.service.repository.SmsInfoRepository; import com.services.report.service.request.screen.SmsInfoQueryRequest; import com.services.report.service.response.screen.SmsInfoQueryResponse; import com.services.report.service.service.ReporterService; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import java.util.ArrayList; import java.util.Date; import java.util.List; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class ScreenQueryControllerTest { @Mock private ReporterService reporterService; @Mock private SmsInfoRepository smsInfoRepository; @InjectMocks private ScreenQueryController screenQueryController = new ScreenQueryController(); private SmsInfoQueryRequest smsInfoQueryRequest; private SmsInfoQueryResponse smsInfoQueryResponse; private SmsInfo smsInfo; private List<SmsInfo> list; @Test public void testGetSmsInfoByQuery_whenRequestBodyIsValidReturnResponseStatusSuccess() { getsmsInfoQueryRequest(); list = new ArrayList<>(); getSmsInfo(); list.add(smsInfo); when(smsInfoRepository.query(smsInfoQueryRequest)).thenReturn(list); smsInfoQueryResponse = screenQueryController.getSmsInfoByQuery(smsInfoQueryRequest); assertEquals("SUCCESS", smsInfoQueryResponse.getStatus()); } public SmsInfoQueryRequest getsmsInfoQueryRequest(){ smsInfoQueryRequest = new SmsInfoQueryRequest(); smsInfoQueryRequest.setPartnerId("99"); smsInfoQueryRequest.setCompanyId("1"); smsInfoQueryRequest.setStartDate(new Date()); smsInfoQueryRequest.setEndDate(new Date()); return smsInfoQueryRequest; } public SmsInfo getSmsInfo() { smsInfo = new SmsInfo(); smsInfo.setId("1"); smsInfo.setVersion(1L); smsInfo.setUpdateDate(new Date()); smsInfo.setCreateDate(new Date()); smsInfo.setStatus("completed"); smsInfo.setPartnerId("999"); smsInfo.setCompanyId("888"); smsInfo.setUser("admin"); smsInfo.setRequestName("Request-3"); smsInfo.setStartDate(new Date()); smsInfo.setEndDate(new Date()); smsInfo.setMessage("testt"); smsInfo.setAlfanumeric("alfanumeric"); smsInfo.setBmspGroupId("1"); smsInfo.setBmspJobIds(new ArrayList<>()); smsInfo.setReceiverCount(1L); smsInfo.setSuccessCount(2L); smsInfo.setFailureCount(3L); smsInfo.setWaitingCount(4L); smsInfo.setLastUpdateEventTime(5L); smsInfo.setErrorReason(new ArrayList<>()); smsInfo.setRequestType("Request-Type"); smsInfo.setInfos(new ArrayList<>()); smsInfo.setSource("Kaynak"); smsInfo.setSmsType("Sms"); return smsInfo; } }
根因说明
出现「单方法运行正常、整类运行报不必要桩错误」的核心原因是测试代码存在两个不稳定逻辑,触发了Mockito默认的严格桩校验:
@InjectMocks字段手动实例化导致注入行为不确定:手动给@InjectMocks标注的控制器赋值new ScreenQueryController()时,Mockito的依赖注入时机不受控。单方法运行时,执行顺序为「初始化Mock对象→注入Mock到控制器实例」,桩可以正常命中;整类运行时,类加载阶段就完成了控制器字段的初始化,此时Mock对象还未创建,控制器内持有的SmsInfoRepository实例不是Mock生成的代理对象,后续给Mock打的桩完全不会被调用,被Mockito判定为无效桩抛出异常。- 类级共享可变测试数据导致参数匹配失效:测试请求、测试实体、结果列表都定义为类成员变量,且辅助方法直接修改类成员状态,整类运行时多测试方法共享同一份可变数据,会出现参数匹配不命中、桩逻辑未触发的问题。
另外代码中还存在一个隐性问题:声明了ReporterService的Mock但未做任何打桩,如果控制器实际是通过Service层调用查询逻辑,而非直接调用Repository,给Repository打桩本身就是无效逻辑,也会触发不必要桩报错。
修复步骤
- 修正
@InjectMocks写法:删除手动实例化的代码,交给Mockito完成控制器实例创建和依赖注入,保证注入逻辑可控。// 修正后写法 @InjectMocks private ScreenQueryController screenQueryController; - 增加测试隔离逻辑:删除类级共享的测试数据成员变量,新增
@Before标注的初始化方法,保证每个测试方法执行前都重置Mock状态、拿到全新的测试上下文,避免测试数据互相污染。
把原来直接修改类成员的辅助方法改成纯构建方法,每次调用返回全新的测试对象实例,不要操作共享变量:import org.junit.Before; import org.mockito.MockitoAnnotations; @Before public void setUp() { MockitoAnnotations.openMocks(this); }// 示例:构建测试请求 private SmsInfoQueryRequest buildTestRequest() { SmsInfoQueryRequest request = new SmsInfoQueryRequest(); request.setPartnerId("99"); request.setCompanyId("1"); request.setStartDate(new Date()); request.setEndDate(new Date()); return request; } - 校验桩的目标对象是否正确:梳理
ScreenQueryController.getSmsInfoByQuery的实际调用链路,如果方法内部是通过注入的ReporterService完成查询,就把桩打在reporterService对应的方法上,不要给未被直接调用的依赖打桩。 - (不推荐)临时规避方案:如果确认逻辑没有问题,可以把Runner替换为宽松校验模式,关闭不必要桩检查,但优先排查前面的注入和桩匹配问题,不要直接关校验掩盖代码问题。
@RunWith(MockitoJUnitRunner.Silent.class)
内容的提问来源于stack exchange,提问作者Defne Sabancı
相关产品推荐
相关产品推荐

