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

如何实现Spring项目中insertEvent方法的100%测试覆盖率

问题:Spring中PreparedStatementCallback代码块未执行,测试覆盖率无法达100%

我在Spring项目里调试时发现insertEvent方法中的doInPreparedStatement没有执行,导致PreparedStatementCallback代码块没被覆盖,测试覆盖率达不到100%。

核心业务代码

@Override
public int insertEvent(JobStatusDTO jobStatusDTO) throws AccountUpdateException {
    LOGGER.info("In insertAccountUpdateEvent- {}", jobStatusDTO);
    try {
        jdbcTemplate.execute("INSERT INTO EVENT_LOG (JOB_EXECUTION_ID, FILE_NAME, STATUS, TOTAL_EVENTS_RECD, TOTAL_EVENTS_PROCESSED, TOTAL_EVENTS_FAILED, FILE_PROCESS_START_TIME, FILE_PROCESS_END_TIME, PROCESS_MESSAGE_N) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
            new PreparedStatementCallback<Boolean>() {
                @Override
                public Boolean doInPreparedStatement(PreparedStatement ps) throws SQLException, DataAccessException {
                    ps.setLong(1, jobStatusDTO.getJobId());
                    ps.setString(2, jobStatusDTO.getFileName());
                    ps.setString(3, jobStatusDTO.getJobStatus());
                    ps.setLong(4, jobStatusDTO.getTotalEvents());
                    ps.setLong(5, jobStatusDTO.getTotalSuccessEvents());
                    ps.setLong(6, jobStatusDTO.getTotalFailedEvents());
                    ps.setTimestamp(7, new Timestamp(jobStatusDTO.getJobStartTime().getTime()));
                    ps.setTimestamp(8, new Timestamp(jobStatusDTO.getJobEndTime().getTime()));
                    ps.setString(9, processErrorMessage(jobStatusDTO));
                    return ps.execute();
                }
            });
    } catch (DataAccessException e) {
        throw new AccountUpdateException(Error.ERR_080.getErrorMessage(), e.getMessage());
    } catch (Exception e) {
        throw new AccountUpdateException(Error.ERR_081.getErrorMessage(), e.getMessage());
    }
    return 0;
}

我编写的单元测试

class AccountUpdateAuditDAOImplTest {

    @InjectMocks
    AccountUpdateAuditDAOImpl accountUpdateAuditDAOImpl;

    @Mock
    JdbcTemplate jdbcTemplate;

    @Mock
    ResultSet set;

    @Mock
    NamedParameterJdbcTemplate namedTemplate;

    @SuppressWarnings("rawtypes")
    @Captor
    ArgumentCaptor argCaptor;

    @BeforeEach
    void setup() throws Exception {
        MockitoAnnotations.openMocks(this);
    }

    @SuppressWarnings("unchecked")
    @Test
    void testInsertAccountUpdateEvent() throws AccountUpdateException, Exception {
        when(jdbcTemplate.execute(Mockito.anyString(), Mockito.any(PreparedStatementCallback.class))).thenReturn(true);
        JobStatusDTO dTO = createMockDTO();
        PreparedStatement ps = mock(PreparedStatement.class);
        ps.setLong(1, dTO.getJobId());
        List<JobStatusDTO> list = new ArrayList<JobStatusDTO>();
        dTO.setJobId(1);
        list.add(dTO);
        
        List<Map<String, Object>> value = new ArrayList<Map<String, Object>>();
        Map<String, Object> map =new HashMap<String, Object>();
        map.put("Job_Id", "1");
        map.put("File_Name", "RTAU_Btach");
        map.put("Job_Status", "Completed");
        map.put("Total_Event", "10");
        map.put("Success_Event", "7");
        map.put("Failure_Event", "3");
        value.add(map);
        
        Mockito.when(namedTemplate.queryForList(Mockito.anyString(), Mockito.anyMap())).thenReturn(value);
        assertDoesNotThrow(() -> accountUpdateAuditDAOImpl.insertAccountUpdateEvent(dTO));
    }

    private JobStatusDTO createMockDTO() {
        JobStatusDTO jobStatusDTO = new JobStatusDTO();
        jobStatusDTO.setFileName("rtau_pulse_20220624104610853.txt");
        jobStatusDTO.setJobStatus("PARTIALLY COMPLETED");
        jobStatusDTO.setProcessMessage("Passed");
        jobStatusDTO.setTotalSuccessEvents(15l);
        return jobStatusDTO;
    }
}

测试问题分析与修正方案

当前测试的问题

  1. 未触发回调执行:直接mockjdbcTemplate.execute返回true,没有调用PreparedStatementCallback里的doInPreparedStatement方法,导致回调代码块完全没被覆盖。
  2. 无效代码冗余:namedTemplate、ResultSet、list、value等对象和insertEvent方法逻辑无关,属于冗余代码,会干扰测试逻辑。
  3. DTO数据不完整:createMockDTO未填充jobId、totalEvents、jobStartTime等必填字段,执行回调时会抛出空指针异常。

修正后的测试代码

class AccountUpdateAuditDAOImplTest {

    @InjectMocks
    AccountUpdateAuditDAOImpl accountUpdateAuditDAOImpl;

    @Mock
    JdbcTemplate jdbcTemplate;

    @Mock
    PreparedStatement preparedStatement;

    @Captor
    ArgumentCaptor<PreparedStatementCallback<Boolean>> callbackCaptor;

    @BeforeEach
    void setup() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    void testInsertAccountUpdateEvent() throws AccountUpdateException, SQLException {
        // 1. 准备完整的测试DTO,避免空指针
        JobStatusDTO dto = createCompleteMockDTO();

        // 2. mock jdbcTemplate.execute并捕获回调对象
        when(jdbcTemplate.execute(anyString(), callbackCaptor.capture())).thenReturn(true);

        // 3. 调用目标方法
        assertDoesNotThrow(() -> accountUpdateAuditDAOImpl.insertEvent(dto));

        // 4. 获取捕获的回调,手动执行doInPreparedStatement覆盖代码块
        PreparedStatementCallback<Boolean> callback = callbackCaptor.getValue();
        callback.doInPreparedStatement(preparedStatement);

        // 5. 验证PreparedStatement的set方法是否按预期调用
        verify(preparedStatement).setLong(1, dto.getJobId());
        verify(preparedStatement).setString(2, dto.getFileName());
        verify(preparedStatement).setString(3, dto.getJobStatus());
        verify(preparedStatement).setLong(4, dto.getTotalEvents());
        verify(preparedStatement).setLong(5, dto.getTotalSuccessEvents());
        verify(preparedStatement).setLong(6, dto.getTotalFailedEvents());
        verify(preparedStatement).setTimestamp(7, new Timestamp(dto.getJobStartTime().getTime()));
        verify(preparedStatement).setTimestamp(8, new Timestamp(dto.getJobEndTime().getTime()));
        verify(preparedStatement).setString(9, accountUpdateAuditDAOImpl.processErrorMessage(dto));
        verify(preparedStatement).execute();
    }

    @Test
    void testInsertAccountUpdateEvent_DataAccessException() {
        JobStatusDTO dto = createCompleteMockDTO();

        // mock抛出DataAccessException,覆盖第一个catch块
        when(jdbcTemplate.execute(anyString(), any(PreparedStatementCallback.class)))
                .thenThrow(new DataAccessResourceFailureException("DB连接失败"));

        AccountUpdateException exception = assertThrows(AccountUpdateException.class,
                () -> accountUpdateAuditDAOImpl.insertEvent(dto));
        assertEquals(Error.ERR_080.getErrorMessage(), exception.getMessage());
    }

    @Test
    void testInsertAccountUpdateEvent_GeneralException() {
        JobStatusDTO dto = createCompleteMockDTO();

        // mock抛出普通Exception,覆盖第二个catch块
        doThrow(new RuntimeException("未知异常"))
                .when(jdbcTemplate).execute(anyString(), any(PreparedStatementCallback.class));

        AccountUpdateException exception = assertThrows(AccountUpdateException.class,
                () -> accountUpdateAuditDAOImpl.insertEvent(dto));
        assertEquals(Error.ERR_081.getErrorMessage(), exception.getMessage());
    }

    // 生成完整的测试DTO,填充所有必要字段
    private JobStatusDTO createCompleteMockDTO() {
        JobStatusDTO jobStatusDTO = new JobStatusDTO();
        jobStatusDTO.setJobId(1L);
        jobStatusDTO.setFileName("rtau_pulse_20220624104610853.txt");
        jobStatusDTO.setJobStatus("PARTIALLY COMPLETED");
        jobStatusDTO.setProcessMessage("Passed");
        jobStatusDTO.setTotalEvents(20L);
        jobStatusDTO.setTotalSuccessEvents(15L);
        jobStatusDTO.setTotalFailedEvents(5L);
        jobStatusDTO.setJobStartTime(new Date());
        jobStatusDTO.setJobEndTime(new Date());
        return jobStatusDTO;
    }
}

关键优化点

  • 通过ArgumentCaptor捕获PreparedStatementCallback对象,手动调用doInPreparedStatement方法,确保回调代码块被覆盖。
  • 补充异常场景测试,覆盖两个catch分支,进一步提升测试覆盖率。
  • 删除所有冗余代码,简化测试逻辑,只保留和insertEvent相关的mock和验证。
  • 完善DTO数据填充,避免空指针异常导致测试失败。

内容的提问来源于stack exchange,提问作者Robin kumar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 23:35:22