【mockito高级篇】mock私有、静态、变量、异常等场景

在上一篇文章中,我们介绍了如何使用mockito对public方法进行模拟(mocking)。通过模拟方法,我们可以避免访问外部资源,例如数据库、RPC和HTTP请求,从而能够更高效地进行单元测试,并验证我们编写的方法是否正确。

然而,有些场景需要特殊处理,例如私有方法、静态方法、变量和异常处理。接下来,我们将介绍如何处理这些特殊场景。

环境准备

依赖选择

对于私有和静态方法的处理需引入增强依赖进行mock。常用的依赖有powermock-api-mockitopowermock-api-mockito2、mockito-inline,但据我所查powermock-api-mockitopowermock-api-mockito2分别于2018年和2020年就不再更新,在这里我推荐使用mockito-inline

依赖引入

版本选择

首先,mockito-core依赖在3.4.0版本开始才将mockStatic方法引入,这意味着,从Mockito 3.4.0版本开始,用户可以直接使用Mockito来模拟静态方法,而无需依赖其他工具(如之前常用的PowerMock)。但要使用mockStatic方法,你需要确保你的项目中包含mockito-inline依赖,因为mockStatic功能是由mockito-inline模块提供的。

依赖详情

本次是采用junit5+mockito进行的单元测试,依赖jar如下

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-inline</artifactId>
    <version>3.12.4</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>3.12.4</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>3.12.4</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.6.3</version>
    <scope>test</scope>
</dependency>
关键注意事项
  1. 依赖冲突

    1. mockito-inline 会隐式包含 mockito-core无需重复声明

    2. 若项目中已有 mockito-core,需确保版本与 mockito-inline 完全一致

版本选择建议
  • 新项目:直接使用 Mockito 5.x(最新版兼容性和性能更优)。

  • 旧项目升级:建议逐步升级到 4.x/5.x,避免从 2.x 直接跳跃。

特殊场景处理

静态方法

在编码过程中常常把一些工具类方法、工厂方法以及常量写到静态方法中,便于重复使用。但在单元测试时这些方法是不便于mock。以下为静态方法mock示例。

1. 有返参


public interface YmContextRegister {

     /**
     * 获取客户端
     *
     * @param portId 接口ID
     * @return 客户端处理器
     * @throws Exception 异常
     */
    @NonNull
    public static BatchComputeHelper getBatchComputeHelper(@NonNull Long portId) throws Exception {
        if (batchComputeHelperMap.containsKey(portId)) {
            return batchComputeHelperMap.get(portId);
        }
        batchComputeHelperMap.putIfAbsent(portId, buildBatchComputeHelper(portId));
        return batchComputeHelperMap.get(portId);
    }

    private static BatchComputeHelper buildBatchComputeHelper(Long portId) throws Exception {
        PortConfig portConfig = PortConfigFactory.getPortConfig(portId);
        if (portConfig instanceof AsyncPortConfig) {
            return new BatchComputeHelper(portConfig.getAppId(), portConfig.getTokenId());
        }else{
            throw new RuntimeException("接口失败");
        }
}

2. 无返参

public class TableNameHandlerContainer {

    private static final Set<String> DYNAMIC_TABLE_NAME_PREFIX_SET = new HashSet<>();
    private static final ThreadLocal<Map<String, String>> DYNAMIC_TABLE_NAME_HOLDERS = new ThreadLocal<>();

    private static final Map<String, TableNameHandler> TABLE_NAME_HANDLER_MAP = new HashMap<>();
    public static final String TB_PREFIX_TARGET_DATA="tb_target_data";

    public static final String TB_PREFIX_SOURCE_DATA="tb_source_data";
    static {
        TableNameHandler tableNameHandler = (sql, tableName) -> {
            Map<String, String> tableNameMap = DYNAMIC_TABLE_NAME_HOLDERS.get();
            return tableNameMap.getOrDefault(tableName, tableName);
        };
        DYNAMIC_TABLE_NAME_PREFIX_SET.add(TB_PREFIX_TARGET_DATA);
        DYNAMIC_TABLE_NAME_PREFIX_SET.add(TB_PREFIX_SOURCE_DATA);
        for (String tbName : DYNAMIC_TABLE_NAME_PREFIX_SET) {
            TABLE_NAME_HANDLER_MAP.put(tbName, tableNameHandler);
        }
    }

    public static void setTableName(String prefix, String tableName) {
        Map<String, String> tableNameMap = DYNAMIC_TABLE_NAME_HOLDERS.get();
        if (tableNameMap == null) {
            tableNameMap = new HashMap<>();
            DYNAMIC_TABLE_NAME_HOLDERS.set(tableNameMap);
        }
        tableNameMap.put(prefix, tableName);
    }

    public static void clearTableName(String prefix) {
        Map<String, String> tableNameMap = DYNAMIC_TABLE_NAME_HOLDERS.get();
        if (tableNameMap != null) {
            tableNameMap.remove(prefix);
        }
    }

    public static  Map<String, TableNameHandler> getHandlerMap(){
        return TABLE_NAME_HANDLER_MAP;
    }

}

public interface TableNameHandler {
    String dynamicTableName(String sql, String tableName);
}

3. 静态方法mock示例

class YmRegisterTest {
    @Mock
    BatchComputeHelper mockBatchComputeHelper;

    MockedStatic<YmContextRegister> ymContextRegisterMockedStatic;
    SelectComputeTask2ResponseBody.SelectComputeTask2ResponseBodyData responseBodyData;
    MockedStatic<TableNameHandlerContainer> mockedStatic;

    @SneakyThrows
    @BeforeEach
    void setUp() {
        ymContextRegisterMockedStatic = mockStatic(YmContextRegister.class);
        mockedStatic = mockStatic(TableNameHandlerContainer.class);
        /**
         * 需要在mockStatic之后
         * MockitoAnnotations.initMocks(this) 方法用于初始化带有 @Mock、@InjectMocks 等注解的字段,而 mockStatic 方法用于模拟静态方法的行为。根据 Mockito 的工作原理:
         * mockStatic 必须在 initMocks 之前调用,以确保静态方法的模拟生效。
         * 如果 initMocks 在 mockStatic 之前调用,可能会导致静态方法的模拟失效,因为此时 Mockito 尚未正确初始化 Mock 对象。
         * 因此,initMocks 方法应该放在 mockStatic 之后。
         */
        MockitoAnnotations.initMocks(this);

        //set up
        //有返参静态方法的mock  方式1
        when(YmContextRegister.getBatchComputeHelper(any())).thenAnswer(invocationOnMock -> mockBatchComputeHelper);
        //方式2
        ymContextRegisterMockedStatic.when(() -> YmContextRegister.getBatchComputeHelper(any())).thenAnswer(invocationOnMock -> mockBatchComputeHelper);

        //对无返参的静态方法mock
        mockedStatic.when(() -> TableNameHandlerContainer.setTableName(anyString(), anyString())).thenAnswer(invocationOnMock -> {
            return null;
        });
    }

    @Test
    void test() throws Exception {
        responseBodyData = new SelectComputeTask2ResponseBody.SelectComputeTask2ResponseBodyData();
        
        when(mockBatchComputeHelper.getBatchComputeTask(any(), any())).thenAnswer(invocationOnMock -> responseBodyData);


        BatchComputeHelper batchComputeHelper = YmContextRegister.getBatchComputeHelper(1L);
        SelectComputeTask2ResponseBody.SelectComputeTask2ResponseBodyData responseBodyData = batchComputeHelper.getBatchComputeTask(1L, null);
        log.info(JSON.toJSONString(responseBodyData));
        
    }

    @AfterEach
    public void tearDown() {
        // 恢复静态方法调用
        if (ymContextRegisterMockedStatic != null) {
            ymContextRegisterMockedStatic.close();
        }
        if (mockedStatic != null) {
            mockedStatic.close();
        }
    }
}

私有方法

在做单元测试时,我们有时需要对private修饰的方法进行测试,通过public方法作为入口又嫌流程太长不好测试,可以通过利用反射机制,对private方法进行mock。

注意:

1、如果方法名称不存在或参数类型不正确的话,会报错,不会返回null

2、因对private修饰的方法mock,需要用getDeclaredMethod

代码示例

private PageContext getPageContext(PageContext dataSegment, PlanContext planContext) {
        PageContext pageQueryContext = new PageContext();
        Long querySize = dataSegment.getEndId() - dataSegment.getStartId() + 1;
        pageQueryContext.setStartId(dataSegment.getStartId());
        pageQueryContext.setEndId(dataSegment.getEndId());
        //按照分批次处理的逻辑 需要处理多少批次
        long partPageCount = PageContext.getPageCount(querySize, planContext.getBatchQueryPageSize());
        pageQueryContext.setPageCount(partPageCount);
        pageQueryContext.setTotalCount(querySize);
        log.info("当前数据段分配到的数据量 querySize: {}, batchQueryPageSize: {}, pageCount:{}, start: {}, endId: {}",
                querySize, planContext.getBatchQueryPageSize(), pageQueryContext.getPageCount(), pageQueryContext.getStartId(), pageQueryContext.getEndId());
        return pageQueryContext;
    }

mock示例

利用反射机制对private方法进行调用

@ExtendWith(MockitoExtension.class)
class AsyncTriggerTest {
    @InjectMocks
    private AsyncTrigger asyncTrigger;

    @SneakyThrows
    @BeforeEach
    void setUp() {
        
        /**
         * MockitoAnnotations.initMocks(this) 方法用于初始化带有 @Mock、@InjectMocks 等注解的字段,而 mockStatic 方法用于模拟静态方法的行为。根据 Mockito 的工作原理:
         * mockStatic 必须在 initMocks 之前调用,以确保静态方法的模拟生效。
         * 如果 initMocks 在 mockStatic 之前调用,可能会导致静态方法的模拟失效,因为此时 Mockito 尚未正确初始化 Mock 对象。
         * 因此,initMocks 方法应该放在 mockStatic 之后。
         */
        MockitoAnnotations.initMocks(this);
        
    }
    /**
     * 私有方法mock
     */
    @Test
    void getPageContextTest() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        PlanContext planContext = PlanContext.builder()
                .planConfig(new AsyncPlanConfig())
                .exchangePlanDetailWrapList(null)
                .scheduleContext(ScheduleContext.builder()
                        .planIds(Collections.singletonList(0L))
                        .build())
                .build();
        PageContext pageContext = new PageContext();
        pageContext.setTotalCount(1L);

        Class<? extends AsyncTrigger> clazz = asyncTrigger.getClass();
        // 通过指定方法名称和参数类型的方法来获取Method对象(注意: 如果方法名称不存在或参数类型不正确的话,会报错,不会返回null)
        // 注意:这里测试的是 private 修饰的私有方法,需要用 getDeclaredMethod
        // getPageContext 是需要测试的方法名,后面为该方法需要的参数类型
        Method method = clazz.getDeclaredMethod("getPageContext", PageContext.class, PlanContext.class);
        //设置方法可访问
        method.setAccessible(true);
        PageContext result = (PageContext) method.invoke(asyncTrigger, pageContext, planContext);
        Assertions.assertEquals(1L, result.getPageCount());
    }
}

lambda表达式

在使用mybatis-plus框架操作数据库时,常用到lambda表达式进行sql的组装。但针对这种表达式不同的条件时并不是好mock的。首先需对表达式中的方法提前mock。

注意:mock的方法数量和类型不能比实际用到的少不然会报NullPointerExecption异常

代码示例

private PlanExecRecord getPlanExecRecord(PlanContext planContext) {
        return planExecRecordManager.getOne(new LambdaQueryWrapper<PlanExecRecord>()
                .eq(PlanExecRecord::getPlanId, planContext.getPlanId())
                .eq(PlanExecRecord::getYn, YesOrNoEnum.NO.getCode()).orderByDesc(PlanExecRecord::getId).last("limit 1"));
    }

mock示例

@ExtendWith(MockitoExtension.class)
class AsyncTriggerTest {
    @Mock
    private PlanExecRecordManager mockPlanExecRecordManager;
    @InjectMocks
    private AsyncTrigger asyncTrigger;

    @SneakyThrows
    @BeforeEach
    void setUp() {
        MockitoAnnotations.initMocks(this);

        // Configure PlanExecRecordManager.getOne(...).
        //mockConstruction 方法用于创建类的 mock 实例,并允许你配置这些 mock 实例的行为
        mockConstruction(LambdaQueryWrapper.class, (mock, context) -> {
            //对表达式中的方法进行mock
            when(mock.eq(any(), any())).thenReturn(mock);
            when(mock.ne(any(), any())).thenReturn(mock);
            when(mock.in(any())).thenReturn(mock);
            when(mock.orderByAsc(any())).thenReturn(mock);
            when(mock.orderByDesc(any())).thenReturn(mock);
            when(mock.last(any())).thenReturn(mock);
        });
    }
    
    /**
     * mock私有方法
    */
    @Test
    void privateMethodTest() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        PlanExecRecord planExecRecord = PlanExecRecord.builder()
                .id(0L)
                .planId(0L)
                .state(1)
                .yn(0)
                .build();
        when(mockPlanExecRecordManager.getOne(any())).thenReturn(planExecRecord);
        Class<? extends AsyncTrigger> clazz = asyncTrigger.getClass();
        // 通过指定方法名称和参数类型的方法来获取Method对象(注意: 如果方法名称不存在或参数类型不正确的话,会报错,不会返回null)
        // 注意:这里测试的是 private 修饰的私有方法,需要用 getDeclaredMethod
        // setUserInfo 是需要测试的方法名,后面为该方法需要的参数类型
        Method method = clazz.getDeclaredMethod("getPlanExecRecord", PlanContext.class);
        //设置方法可访问
        method.setAccessible(true);
        AsyncPlanConfig asyncPlanConfig = new AsyncPlanConfig();
        asyncPlanConfig.setSourceData(new SourceDataConfig());
        PlanContext planContext = PlanContext.builder().planConfig(asyncPlanConfig).build();
        PlanExecRecord result = (PlanExecRecord) method.invoke(asyncTrigger, planContext);
        Assertions.assertEquals(planExecRecord, result);
    }
}

变量

在实际开发中我们采用一些动态配置以达到不同场景下的灵活使用。在对方法mock时,由于是通过@InjectMocks注解创建一个被测试类的实例,那么类中的变量是没法加载获取配置的,可以使用反射机制进行变量的mock。这样就可以在mock类中正常使用变量。

代码示例

@Value("${mock.value}")
private String mockValue;

mock示例

@ExtendWith(MockitoExtension.class)
class AsyncTriggerTest {
    @Mock
    private PlanExecRecordManager mockPlanExecRecordManager;
    @InjectMocks
    private AsyncTrigger asyncTrigger;

    @SneakyThrows
    @BeforeEach
    void setUp() {
        MockitoAnnotations.initMocks(this);

        // Configure PlanExecRecordManager.getOne(...).
        //mockConstruction 方法用于创建类的 mock 实例,并允许你配置这些 mock 实例的行为
        mockConstruction(LambdaQueryWrapper.class, (mock, context) -> {
            //对表达式中的方法进行mock
            when(mock.eq(any(), any())).thenReturn(mock);
            when(mock.ne(any(), any())).thenReturn(mock);
            when(mock.in(any())).thenReturn(mock);
            when(mock.orderByAsc(any())).thenReturn(mock);
            when(mock.orderByDesc(any())).thenReturn(mock);
            when(mock.last(any())).thenReturn(mock);
        });
        //apollo或nacos的配置
        ReflectionTestUtils.setField(asyncTrigger, "mockValue", "mockValue");
    }
    
    /**
     * mock私有方法
    */
    @Test
    void privateMethodTest() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        PlanExecRecord planExecRecord = PlanExecRecord.builder()
                .id(0L)
                .planId(0L)
                .state(1)
                .yn(0)
                .build();
        when(mockPlanExecRecordManager.getOne(any())).thenReturn(planExecRecord);
        Class<? extends AsyncTrigger> clazz = asyncTrigger.getClass();
        // 通过指定方法名称和参数类型的方法来获取Method对象(注意: 如果方法名称不存在或参数类型不正确的话,会报错,不会返回null)
        // 注意:这里测试的是 private 修饰的私有方法,需要用 getDeclaredMethod
        // setUserInfo 是需要测试的方法名,后面为该方法需要的参数类型
        Method method = clazz.getDeclaredMethod("getPlanExecRecord", PlanContext.class);
        //设置方法可访问
        method.setAccessible(true);
        AsyncPlanConfig asyncPlanConfig = new AsyncPlanConfig();
        asyncPlanConfig.setSourceData(new SourceDataConfig());
        PlanContext planContext = PlanContext.builder().planConfig(asyncPlanConfig).build();
        PlanExecRecord result = (PlanExecRecord) method.invoke(asyncTrigger, planContext);
        Assertions.assertEquals(planExecRecord, result);
    }
}

异常

在对方法单元测试时,为了验证方法处理逻辑是否正常,需要将Execption场景也许覆盖到。可以采用doThrow()对执行的逻辑模拟抛异常

代码示例

当执行查询逻辑时让其抛异常,验证异常处理逻辑

private PlanExecRecord getPlanExecRecord(PlanContext planContext) {
        return planExecRecordManager.getOne(new LambdaQueryWrapper<PlanExecRecord>()
                .eq(PlanExecRecord::getPlanId, planContext.getPlanId())
                .eq(PlanExecRecord::getYn, YesOrNoEnum.NO.getCode()).orderByDesc(PlanExecRecord::getId).last("limit 1"));
    }

mock示例

@ExtendWith(MockitoExtension.class)
class AsyncTriggerTest {
    @Mock
    private PlanExecRecordManager mockPlanExecRecordManager;
    @InjectMocks
    private AsyncTrigger asyncTrigger;

    @SneakyThrows
    @BeforeEach
    void setUp() {
        MockitoAnnotations.initMocks(this);

        // Configure PlanExecRecordManager.getOne(...).
        //mockConstruction 方法用于创建类的 mock 实例,并允许你配置这些 mock 实例的行为
        mockConstruction(LambdaQueryWrapper.class, (mock, context) -> {
            //对表达式中的方法进行mock
            when(mock.eq(any(), any())).thenReturn(mock);
            when(mock.ne(any(), any())).thenReturn(mock);
            when(mock.in(any())).thenReturn(mock);
            when(mock.orderByAsc(any())).thenReturn(mock);
            when(mock.orderByDesc(any())).thenReturn(mock);
            when(mock.last(any())).thenReturn(mock);
        });
        //apollo或nacos的配置
        ReflectionTestUtils.setField(asyncTrigger, "mockValue", "mockValue");
    }
    
    /**
     * mock私有方法
    */
    @Test
    void privateMethodTest() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        PlanExecRecord planExecRecord = PlanExecRecord.builder()
                .id(0L)
                .planId(0L)
                .state(1)
                .yn(0)
                .build();
        when(mockPlanExecRecordManager.getOne(any())).thenReturn(planExecRecord);
        Class<? extends AsyncTrigger> clazz = asyncTrigger.getClass();
        // 通过指定方法名称和参数类型的方法来获取Method对象(注意: 如果方法名称不存在或参数类型不正确的话,会报错,不会返回null)
        // 注意:这里测试的是 private 修饰的私有方法,需要用 getDeclaredMethod
        // setUserInfo 是需要测试的方法名,后面为该方法需要的参数类型
        Method method = clazz.getDeclaredMethod("getPlanExecRecord", PlanContext.class);
        //设置方法可访问
        method.setAccessible(true);
        AsyncPlanConfig asyncPlanConfig = new AsyncPlanConfig();
        asyncPlanConfig.setSourceData(new SourceDataConfig());
        PlanContext planContext = PlanContext.builder().planConfig(asyncPlanConfig).build();
        PlanExecRecord result = (PlanExecRecord) method.invoke(asyncTrigger, planContext);
        Assertions.assertEquals(planExecRecord, result);

        // 模拟抛出异常
        doThrow(new RuntimeException("mock error")).when(mockPlanExecRecordManager.getOne(any()));
        result = (PlanExecRecord) method.invoke(asyncTrigger, planContext);
        Assertions.assertEquals(planExecRecord, result);
    }
}

通过以上方式,能够提高单元测试的场景覆盖率,可以更全面地覆盖代码的各种不同情况,从而提高测试的质量和可靠性,更好地验证的代码逻辑是否合理。这样能够帮助我们及早发现和解决潜在的问题,保证代码的质量和稳定性。

上一篇:【mockito初级篇】Junit + Mockito保姆级集成测试实践

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值