如何用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
相关产品推荐
相关产品推荐

