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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 17:40:31