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

如何用JUnit+Mockito测试Spring Boot的Service与Dao层(含NamedParameterJdbcTemplate)

问题描述

我在Spring Boot项目中采用Controller->Service->Dao的分层架构,需要编写单元测试覆盖所有功能,重点测试Service层和Dao层,其中Service层内部调用3个Dao方法。

当前编写Service层测试时,因内部调用Dao方法出现空指针异常。测试代码执行时在GeneratorRepository的namedParameterJdbcTemplate.queryForObject(existingUtQuery, namedParameters, GeneratorRowMapper)处触发空指针,若不在测试方法中初始化generatorRepository = new GeneratorRepository()和uTService = new UTService()也会触发空指针。

测试代码:

@ExtendWith(MockitoExtension.class)
public class GenerationApplicationTests {

    @InjectMocks
    private GeneratorRepository generatorRepository;
    
    @Mock
    private  NamedParameterJdbcTemplate namedParameterJdbcTemplate;
    
    @Autowired
    private UTService uTService;
    
    
    @Test
    public void Response1() throws Exception {

        generatorRepository = new GeneratorRepository();
        uTService = new UTService();

        Request request = new Request("12345678", "F", 0, "A");
        Generator generator = new Generator("12345678.F","3er45FGTYH");

        System.out.println("utService  :  "+utService);

        when(generatorRepository.findExisting(request)).thenReturn(generator);
        Response response = uTService.getUT(Request);
        assertEquals("12345678.F", response.getValue());
        assertEquals("3er45FGTYH", response.getPre());
    }

}

Service类代码:

@Service
public class UTService {

    @Autowired
    private GeneratorRepository generatorDAO;

    @Autowired
    private TableFunctionRepository tableFunctionRepository;

    public Response getUT(Request request) throws UTNotFoundException {
        Response response = new Response();
        Generator existing = new Generator();
        TableFunction tableData = new TableFunction();
        Generator newGen = new Generator();
        Reasons exclusionInclusion = null;
        existing = generatorDAO.findExistingUT(request);
        if (existing != null) {
            response.setResponse(existing);
            return response;
        }
        tableData = tableFunctionRepository.callTableValuedFunction(request);
        if (tableData == null) {
            String exceptionMessage = request + " not found in database";
            throw new UTNotFoundException(exceptionMessage);
        } else {
            newGen.setInfo(request.getId(), request.getSubId(), request.getRevisionNo(), request.getState());
            exclusionInclusion = checkExclusionInclusionCriteria(tableData);
            newGen.setExclusionInclusionReasonId(exclusionInclusion.getReasonCode());
            newGen = generateUT(tableData, exclusionInclusion, newGen);
            response.setResponse(newGen);
            generatorDAO.saveUT(newGen);
        }
        log.info("finished getUT");
        return response;

    }
    
}

Dao类代码:

@Repository
public class GeneratorRepository 
{
    @Value("${sql.saveutiQuery}")
    private String saveUTQuery;

    @Value("${sql.existingUtiQuery}")
    private String existingUtQuery;


    @Autowired
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

    @Autowired
    private BeanPropertyRowMapper<Generator> generatorRowMapper;

    public int saveUT(Generator generator) {
        int rowsAffected = 0;
        try {
            SqlParameterSource paramSource = new BeanPropertySqlParameterSource(generator);
            rowsAffected = namedParameterJdbcTemplate.update(saveUTQuery, paramSource);
        } catch(Exception e) {
            throw e;
        }
        return rowsAffected;
    }

    public Generator findExistingUT(Request request) {
        Generator ut = new Generator();
        try {
            SqlParameterSource namedParameters = new MapSqlParameterSource()
                    .addValue("No", request.getId(), Types.VARCHAR)
                    .addValue("Sub", request.getSubId(), Types.VARCHAR)
                    .addValue("Revision", request.getRevision(), Types.INTEGER)
                    .addValue("state", request.getState(), Types.VARCHAR);
            ut = namedParameterJdbcTemplate.queryForObject(existingUtQuery, namedParameters, generatorRowMapper);
        } catch (Exception e) {
            throw e;
        }
        return ut;
    }
}


@Repository
public class TableFunctionRepository {

    @Autowired
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

    @Autowired
    private BeanPropertyRowMapper<TableFunction> tableFunctionRowMapper;

    @Value("${sql.DetailsQuery}")
    private String tableDetailsQuery;

    public TableFunction callTableValuedFunction(Request request) {
        TableFunction tableDetails = new TableFunction();
        try {
            SqlParameterSource namedParameters = new MapSqlParameterSource()
                    .addValue("No", request.getId(), Types.VARCHAR)
                    .addValue("Sub", request.getSubId(), Types.VARCHAR)
                    .addValue("Revision", request.getRevision(), Types.INTEGER)
                    .addValue("state", request.getState(), Types.VARCHAR);

            tableDetails = namedParameterJdbcTemplate.queryForObject(tableDetailsQuery, namedParameters, tableFunctionRowMapper);
        } catch (Exception e) {
            throw e;
        }       
        return tableDetails;
    }
}

请问采用JUnit和Mockito编写测试用例,覆盖Service层和Dao层的正确方法是什么?


解决方案

一、Service层单元测试

Service层测试核心是Mock所有Dao依赖,无需启动Spring容器,仅验证业务逻辑。原测试的问题在于手动new对象覆盖了Mockito注入的实例,且依赖注入方式错误。

正确测试代码

@ExtendWith(MockitoExtension.class)
public class UTServiceTest {

    // 待测试的Service,由Mockito注入Mock依赖
    @InjectMocks
    private UTService utService;

    // Mock Service依赖的Dao类
    @Mock
    private GeneratorRepository generatorRepository;
    @Mock
    private TableFunctionRepository tableFunctionRepository;

    // 测试:UT已存在时直接返回结果
    @Test
    public void getUT_WhenUTExists_ReturnsExistingUT() throws UTNotFoundException {
        // 构造测试数据
        Request request = new Request("12345678", "F", 0, "A");
        Generator existingGenerator = new Generator("12345678.F", "3er45FGTYH");

        // Mock Dao方法返回值
        when(generatorRepository.findExistingUT(request)).thenReturn(existingGenerator);

        // 执行Service方法
        Response response = utService.getUT(request);

        // 验证结果
        assertEquals("12345678.F", response.getValue());
        assertEquals("3er45FGTYH", response.getPre());

        // 验证Dao调用次数
        verify(generatorRepository, times(1)).findExistingUT(request);
        verify(tableFunctionRepository, never()).callTableValuedFunction(any(Request.class));
        verify(generatorRepository, never()).saveUT(any(Generator.class));
    }

    // 测试:UT不存在时,调用表函数并保存新UT
    @Test
    public void getUT_WhenUTNotExists_CreatesAndSavesNewUT() throws UTNotFoundException {
        // 构造测试数据
        Request request = new Request("87654321", "F", 1, "B");
        TableFunction tableData = new TableFunction();
        Reasons exclusionReason = new Reasons("REASON_01");
        Generator newGenerator = new Generator("87654321.F", "XYZ789");

        // Mock依赖方法返回值
        when(generatorRepository.findExistingUT(request)).thenReturn(null);
        when(tableFunctionRepository.callTableValuedFunction(request)).thenReturn(tableData);
        
        // Mock Service内部私有方法
        UTService spyService = spy(utService);
        doReturn(exclusionReason).when(spyService).checkExclusionInclusionCriteria(tableData);
        doReturn(newGenerator).when(spyService).generateUT(tableData, exclusionReason, any(Generator.class));

        // 执行Service方法
        Response response = spyService.getUT(request);

        // 验证结果
        assertEquals("87654321.F", response.getValue());
        assertEquals("XYZ789", response.getPre());

        // 验证Dao调用顺序和次数
        verify(generatorRepository, times(1)).findExistingUT(request);
        verify(tableFunctionRepository, times(1)).callTableValuedFunction(request);
        verify(generatorRepository, times(1)).saveUT(newGenerator);
    }

    // 测试:表函数返回null时抛出异常
    @Test(expected = UTNotFoundException.class)
    public void getUT_WhenTableFunctionReturnsNull_ThrowsException() throws UTNotFoundException {
        Request request = new Request("11223344", "F", 2, "C");

        when(generatorRepository.findExistingUT(request)).thenReturn(null);
        when(tableFunctionRepository.callTableValuedFunction(request)).thenReturn(null);

        utService.getUT(request);
    }
}

关键注意事项

  • 用@ExtendWith(MockitoExtension.class)启用Mockito支持,无需启动Spring容器
  • 用@InjectMocks注入待测试Service,@Mock模拟所有依赖的Dao
  • 禁止手动newService或Dao实例,否则Mockito注入会失效
  • 确保Mock的方法名与实际调用一致(原测试中误将findExistingUT写为findExisting)
  • 用verify验证Dao方法的调用次数和参数,确保业务逻辑符合预期
  • 私有方法需Mock时,用spy包装Service实例后再Mock返回值

二、Dao层单元测试

Dao层测试分两种方式:用嵌入式数据库验证真实SQL,或用Mockito模拟JdbcTemplate仅测试逻辑。

方式1:嵌入式数据库测试(推荐)

验证SQL语句正确性,贴近生产环境:

@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
public class GeneratorRepositoryTest {

    @Autowired
    private GeneratorRepository generatorRepository;

    @Autowired
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

    // 测试findExistingUT方法
    @Test
    public void findExistingUT_WhenExists_ReturnsGenerator() {
        // 插入测试数据到H2数据库
        String insertSql = "INSERT INTO generator_table (id, sub_id, revision, state, value, pre) VALUES (:id, :subId, :revision, :state, :value, :pre)";
        MapSqlParameterSource params = new MapSqlParameterSource()
                .addValue("id", "12345678")
                .addValue("subId", "F")
                .addValue("revision", 0)
                .addValue("state", "A")
                .addValue("value", "12345678.F")
                .addValue("pre", "3er45FGTYH");
        namedParameterJdbcTemplate.update(insertSql, params);

        // 执行Dao方法
        Request request = new Request("12345678", "F", 0, "A");
        Generator result = generatorRepository.findExistingUT(request);

        // 验证结果
        assertNotNull(result);
        assertEquals("12345678.F", result.getValue());
        assertEquals("3er45FGTYH", result.getPre());
    }

    // 测试saveUT方法
    @Test
    public void saveUT_InsertsDataSuccessfully() {
        Generator generator = new Generator("87654321.F", "XYZ789");
        generator.setId("87654321");
        generator.setSubId("F");
        generator.setRevision(1);
        generator.setState("B");

        int rowsAffected = generatorRepository.saveUT(generator);

        assertEquals(1, rowsAffected);

        // 验证数据已插入
        String checkSql = "SELECT COUNT(*) FROM generator_table WHERE id = :id AND sub_id = :subId";
        MapSqlParameterSource params = new MapSqlParameterSource()
                .addValue("id", "87654321")
                .addValue("subId", "F");
        Long count = namedParameterJdbcTemplate.queryForObject(checkSql, params, Long.class);
        assertEquals(1, count);
    }
}

方式2:Mockito模拟JdbcTemplate

无需验证SQL,仅测试Dao逻辑:

@ExtendWith(MockitoExtension.class)
public class GeneratorRepositoryMockTest {

    @InjectMocks
    private GeneratorRepository generatorRepository;

    @Mock
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

    @Mock
    private BeanPropertyRowMapper<Generator> generatorRowMapper;

    // 初始化@Value属性
    @BeforeEach
    void setUp() {
        ReflectionTestUtils.setField(generatorRepository, "saveUTQuery", "INSERT INTO generator_table (...) VALUES (...)");
        ReflectionTestUtils.setField(generatorRepository, "existingUtQuery", "SELECT * FROM generator_table WHERE ...");
    }

    @Test
    public void findExistingUT_ReturnsGenerator() {
        Request request = new Request("12345678", "F", 0, "A");
        Generator expectedGenerator = new Generator("12345678.F", "3er45FGTYH");

        when(namedParameterJdbcTemplate.queryForObject(
                anyString(),
                any(SqlParameterSource.class),
                eq(generatorRowMapper)
        )).thenReturn(expectedGenerator);

        Generator result = generatorRepository.findExistingUT(request);

        assertEquals(expectedGenerator, result);
        verify(namedParameterJdbcTemplate, times(1)).queryForObject(
                eq("SELECT * FROM generator_table WHERE ..."),
                any(MapSqlParameterSource.class),
                eq(generatorRowMapper)
        );
    }
}

关键注意事项

  • 嵌入式数据库测试能覆盖SQL逻辑,更可靠
  • 用ReflectionTestUtils设置@Value注入的属性值
  • Mock JdbcTemplate时,需匹配SQL语句、参数源和RowMapper参数

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 16:44:57