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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 18:54:23