如何在Spring测试中基于不同属性值实例化多份同类型Bean?
同一Spring组件多实例不同属性配置的测试优化
问题背景
现有Spring组件:
@Component public class Calculator { @Value("${rate:1.0}") private Double rate; // 包含业务逻辑,以及对rate的校验(比如负值抛出异常) ... }
需要在测试中验证三种场景:
- 未设置
rate,组件使用默认值1.0 - 设置正值
rate,组件正常工作 - 设置负值
rate,组件抛出异常
如果写三个独立的@SpringBootTest类分别配置@TestPropertySource,会因重复代码显得繁琐(实际生产组件逻辑更复杂)。之前尝试用@Configuration类创建不同属性的Bean,但@TestPropertySource的配置未生效:
@Configuration @TestPropertySource(properties = {"rate=2.0"}) @ContextConfiguration(classes = {Calculator.class}) class PositiveRateCalculatorConfig { @Autowired private Calculator calc; @Bean Calculator positiveRateCalculator() { return calc; } }
原因分析
@TestPropertySource是测试类专用注解,仅作用于标注了@Test的测试类,普通@Configuration类上的该注解不会被Spring测试上下文处理。同时,@Autowired注入的Calculator是Spring默认扫描创建的实例,此时上下文并未加载@TestPropertySource中的属性,所以rate仍为默认值。
解决方案
方案1:参数化测试 + 动态属性配置
利用JUnit 5的参数化测试特性,配合动态设置Spring环境属性,一个测试方法即可覆盖三种场景:
@SpringBootTest class CalculatorTest { @ParameterizedTest @CsvSource({ ", 1.0", // 未设置rate,预期默认值1.0 "2.0, 2.0", // 设置正值,预期对应值 "-1.0, null" // 设置负值,预期抛出异常 }) void testCalculator(Double inputRate, Double expectedRate) { if (inputRate != null && inputRate < 0) { // 验证负值场景下组件初始化抛出异常 assertThrows(BeanCreationException.class, () -> { // 手动创建上下文并设置属性 ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(); context.getEnvironment().getPropertySources() .addFirst(new MapPropertySource("test-props", Map.of("rate", inputRate.toString()))); context.register(Calculator.class); context.refresh(); }); return; } // 验证正常场景的属性值与业务逻辑 ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(); if (inputRate != null) { context.getEnvironment().getPropertySources() .addFirst(new MapPropertySource("test-props", Map.of("rate", inputRate.toString()))); } context.register(Calculator.class); context.refresh(); Calculator calculator = context.getBean(Calculator.class); assertEquals(expectedRate, calculator.getRate()); // 这里添加业务逻辑验证,比如calculator.calculate(...) } }
方案2:自定义配置类手动创建多实例
绕过@Value的自动注入,手动创建Calculator实例并设置属性(需处理私有属性或添加Setter方法):
@Configuration class CalculatorMultiInstanceConfig { @Bean("defaultRateCalculator") Calculator defaultRateCalculator() throws Exception { Calculator calc = new Calculator(); setRateAndValidate(calc, null); // 传入null使用默认值 return calc; } @Bean("positiveRateCalculator") Calculator positiveRateCalculator() throws Exception { Calculator calc = new Calculator(); setRateAndValidate(calc, 2.0); return calc; } private void setRateAndValidate(Calculator calc, Double rate) throws Exception { // 反射设置私有rate属性 Field rateField = Calculator.class.getDeclaredField("rate"); rateField.setAccessible(true); rateField.set(calc, rate != null ? rate : 1.0); // 如果组件有@PostConstruct校验逻辑,手动调用触发 Method[] methods = Calculator.class.getDeclaredMethods(); for (Method method : methods) { if (method.isAnnotationPresent(PostConstruct.class)) { method.setAccessible(true); method.invoke(calc); break; } } } }
测试类注入不同实例分别验证:
@SpringBootTest(classes = CalculatorMultiInstanceConfig.class) class CalculatorMultiInstanceTest { @Autowired @Qualifier("defaultRateCalculator") private Calculator defaultCalc; @Autowired @Qualifier("positiveRateCalculator") private Calculator positiveCalc; @Test void testDefaultRate() { assertEquals(1.0, defaultCalc.getRate()); } @Test void testPositiveRate() { assertEquals(2.0, positiveCalc.getRate()); // 验证业务逻辑正确性 } @Test void testNegativeRate() throws Exception { Calculator calc = new Calculator(); // 验证负值触发异常 assertThrows(IllegalArgumentException.class, () -> { // 复用配置类中的属性设置与校验逻辑 Field rateField = Calculator.class.getDeclaredField("rate"); rateField.setAccessible(true); rateField.set(calc, -1.0); Method[] methods = Calculator.class.getDeclaredMethods(); for (Method method : methods) { if (method.isAnnotationPresent(PostConstruct.class)) { method.setAccessible(true); method.invoke(calc); break; } } }); } }
内容的提问来源于stack exchange,提问作者Sadeq
相关产品推荐
相关产品推荐

