JUnit5/Spring Boot:加载Properties文件且Mock依赖的解决方案
我正在为DAO/Repository编写单元测试,想要Mock其中的NamedParameterJdbcTemplate对象,以此验证空值处理等周边业务逻辑的正确性。同时我还希望验证SQL语句组合是否正确,这可以通过doAnswer(...)实现,但SQL查询语句都存储在.properties文件中。
请问有没有办法在不加载该类其他所有依赖的前提下,为测试加载.properties文件?
已尝试方案
我试过给测试类添加多种注解组合:
@ExtendWith(SpringExtension.class) @ExtendWith(MockitoExtension.class) @ContextConfiguration(classes = WidgetRepositoryImpl.class) @TestPropertySource(properties = "sql.properties")
但不管开启或关闭这些注解,总会遇到以下两种情况之一:
- 把
NamedParameterJdbcTemplate作为Bean加载。要是在ContextConfiguration注解里指定AppConfig类可能可行,但会加载所有底层依赖,而我的核心需求是Mock这个字段。 - 加载Repository并Mock
NamedParameterJdbcTemplate,但没加载sql.properties,导致SQL语句为null。
我不想用ReflectionTestUtils.setField(widgetRepository, "namedParameterJdbcTemplate", "<put the sql here>")这种方法,虽然能生效,但SQL查询有很多(示例代码已经大幅简化),手动输入字符串会引入人为错误风险,毕竟测试里用的不是实际properties文件里的内容。
核心问题
有没有办法只为该类加载sql.properties,但不尝试加载NamedParameterJdbcTemplate?
单元测试代码
@ExtendWith(SpringExtension.class) @TestPropertySource(properties = "sql.properties") class WidgetRepositoryImplTest { @Mock private NamedParameterJdbcTemplate namedParameterJdbcTemplate; @Autowired private WidgetRepository widgetRepository; @Test public void testGetWidgetById() { // 构建Mock对象返回假的Widget,并存储SQL字符串用于其他测试 doAnswer(invocation -> { ...省略代码... }).when(namedParameterJdbcTemplate).query(anyString(), anyMap(), any(WidgetMapper.class)); Widget widget = widgetRepository.getWidgetById("asdf-1234"); assertNotNull(widget); } }
Repository代码
@PropertySource("classpath:sql.properties") @Slf4j @Repository public class WidgetRepositoryImpl implements WidgetRepository { @Value("${widget-sql.selects.select-widget-by-id}") private String selectWidgetByIdQuery; @Autowired private NamedParameterJdbcTemplate namedParameterJdbcTemplate; @Override public Widget getWidgetById(String id) { Map<String, String> params = new HashMap<>(); params.put("widgetId", id); List<Widget> results = namedParameterJdbcTemplate.query(selectWidgetByIdQuery, params, new WidgetMapper()); if (results.isEmpty()) { return null; } return results.get(0); } }
sql.properties文件
widget-sql.selects.select-widget-by-id=select * from [WIDGETS] where WIDGET_ID=:widgetId # 还有几十个其他查询语句
方案1:用@MockBean配合Spring测试注解
直接用@MockBean替代@Mock,它会自动替换Spring上下文中的对应Bean,同时保留@TestPropertySource加载properties文件的能力,不会加载真实的NamedParameterJdbcTemplate:
修改测试类如下:
@ExtendWith(SpringExtension.class) @TestPropertySource(locations = "classpath:sql.properties") // 指定properties文件路径 @ContextConfiguration(classes = WidgetRepositoryImpl.class) class WidgetRepositoryImplTest { @MockBean // Spring自动将该Mock注入到Repository private NamedParameterJdbcTemplate namedParameterJdbcTemplate; @Autowired private WidgetRepository widgetRepository; @Test public void testGetWidgetById() { doAnswer(invocation -> { // 获取传入的SQL语句并验证 String sql = invocation.getArgument(0); assertEquals("select * from [WIDGETS] where WIDGET_ID=:widgetId", sql); // 返回模拟结果 return Collections.singletonList(new Widget()); }).when(namedParameterJdbcTemplate).query(anyString(), anyMap(), any(WidgetMapper.class)); Widget widget = widgetRepository.getWidgetById("asdf-1234"); assertNotNull(widget); } }
方案2:手动加载Properties(纯Mockito测试)
如果不想启动Spring上下文,完全用纯Mockito测试,可以手动加载properties文件,再通过ReflectionTestUtils给Repository的SQL字段赋值:
@ExtendWith(MockitoExtension.class) class WidgetRepositoryImplTest { @Mock private NamedParameterJdbcTemplate namedParameterJdbcTemplate; private WidgetRepositoryImpl widgetRepository; @BeforeEach void setUp() throws IOException { widgetRepository = new WidgetRepositoryImpl(); // 注入Mock的JdbcTemplate ReflectionTestUtils.setField(widgetRepository, "namedParameterJdbcTemplate", namedParameterJdbcTemplate); // 手动加载properties文件 Properties properties = new Properties(); try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream("sql.properties")) { properties.load(inputStream); } // 给SQL字段赋值 ReflectionTestUtils.setField(widgetRepository, "selectWidgetByIdQuery", properties.getProperty("widget-sql.selects.select-widget-by-id")); // 其他SQL字段可循环批量处理 } @Test public void testGetWidgetById() { doAnswer(invocation -> { String sql = invocation.getArgument(0); assertEquals("select * from [WIDGETS] where WIDGET_ID=:widgetId", sql); return Collections.singletonList(new Widget()); }).when(namedParameterJdbcTemplate).query(anyString(), anyMap(), any(WidgetMapper.class)); Widget widget = widgetRepository.getWidgetById("asdf-1234"); assertNotNull(widget); } }
方案3:自定义测试配置类
创建仅包含Repository和Mock JdbcTemplate的测试配置类,精确控制加载的Bean:
@Configuration @PropertySource("classpath:sql.properties") static class TestConfig { @Bean public NamedParameterJdbcTemplate namedParameterJdbcTemplate() { return Mockito.mock(NamedParameterJdbcTemplate.class); } @Bean public WidgetRepository widgetRepository() { return new WidgetRepositoryImpl(); } }
测试类使用该配置:
@ExtendWith(SpringExtension.class) @ContextConfiguration(classes = WidgetRepositoryImplTest.TestConfig.class) class WidgetRepositoryImplTest { @Autowired private NamedParameterJdbcTemplate namedParameterJdbcTemplate; @Autowired private WidgetRepository widgetRepository; @Test public void testGetWidgetById() { // 测试逻辑同前 } }
内容的提问来源于stack exchange,提问作者Chris

