You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

运行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默认的严格桩校验:

  1. @InjectMocks字段手动实例化导致注入行为不确定:手动给@InjectMocks标注的控制器赋值new ScreenQueryController()时,Mockito的依赖注入时机不受控。单方法运行时,执行顺序为「初始化Mock对象→注入Mock到控制器实例」,桩可以正常命中;整类运行时,类加载阶段就完成了控制器字段的初始化,此时Mock对象还未创建,控制器内持有的SmsInfoRepository实例不是Mock生成的代理对象,后续给Mock打的桩完全不会被调用,被Mockito判定为无效桩抛出异常。
  2. 类级共享可变测试数据导致参数匹配失效:测试请求、测试实体、结果列表都定义为类成员变量,且辅助方法直接修改类成员状态,整类运行时多测试方法共享同一份可变数据,会出现参数匹配不命中、桩逻辑未触发的问题。
    另外代码中还存在一个隐性问题:声明了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ı

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.02 23:03:31