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

如何在SpringBootTest中让注入实例按原型Bean处理?

问题场景

作为Spring/SpringBoot新手,我定义了玩具汽车经销商接口DealershipService及其实现类DealershipServiceImpl:

  • 实现类包含可修改的revenue字段,通过@Value从配置文件读取初始值1_000_000L
  • 购买车辆会减少revenue,出售车辆则增加revenue

接口代码:

public interface DealershipService {

  Long getRevenue();

  void sell(Car car);

  void buy(Car car);

  List<Car> findByMake(String make);

  List<Car> findByMakeAndModel(String make, String model);

  List<Car> findByMakeModelAndYear(String make, String model, String year);

  List<Car> findByYearRange(String yearLowInclusive, String yearHighInclusive);

  List<Car> findInCostRange(long costLowInclusive, long costHighInclusive);
}

实现类代码:

@Slf4j
@Data
@Service
public class DealershipServiceImpl implements DealershipService {

  @Value("${revenue}")
  private Long revenue;
  private final CarRepository carRepository;

  @Autowired
  public DealershipServiceImpl(CarRepository carRepository) {
    this.carRepository = carRepository;
  }

  @Override
  public void sell(Car car) {
    carRepository.removeFromRepository(car.getVin());
    revenue += car.getCostInUSD();
    log.info("Sold car {}. Current revenue: {}", car, revenue);
  }

  @Override
  public void buy(Car car) {
    if (revenue >= car.getCostInUSD()) {
      carRepository.addToRepository(car);
      revenue -= car.getCostInUSD();
      log.info("Bought car {}. Current revenue: {}", car, revenue);
    } else {
      log.warn("Unable to buy car {}; we're broke!", car);
      throw new InsufficientRevenueException();
    }
  }

  // 省略其他方法实现
}

编写的SpringBoot测试代码:

@SpringBootTest(classes =  TestConfig.class)
public class DealershipServiceIT {

    @Autowired
    private  DealershipService dealershipService;

    @Test
    public void whenNoTransactions_TotalRevenueIsAMil(){
        assertThat(dealershipService.getRevenue(), is(1_000_000L));
    }

    @Test
    public void whenMakingTransactions_revenueIsAffected(){
        Car car = Car.builder().year(2006).vin("1234").costInUSD(10_000L).make("Nissan").model("Sentra").build();
        dealershipService.buy(car);
        assertThat(dealershipService.getRevenue(), is(990_000L));
    }
}

核心问题:注入的dealershipService是单例Bean,测试方法间会互相影响(比如交易测试修改revenue后,无交易测试会读取到修改后的值)。不想将原Bean改为@Scope("prototype")(担心不符合Spring设计规范),希望仅在测试中让该实例按原型Bean处理,即每个测试方法使用新实例,询问可行方案。

可行解决方案

1. 使用@DirtiesContext注解刷新上下文

在测试方法或测试类上添加@DirtiesContext,指定方法执行后刷新Spring上下文,这样每个测试方法都会重新创建所有Bean,保证实例隔离。

修改后的测试代码:

@SpringBootTest(classes = TestConfig.class)
public class DealershipServiceIT {

    @Autowired
    private DealershipService dealershipService;

    @Test
    @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)
    public void whenNoTransactions_TotalRevenueIsAMil(){
        assertThat(dealershipService.getRevenue(), is(1_000_000L));
    }

    @Test
    @DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)
    public void whenMakingTransactions_revenueIsAffected(){
        Car car = Car.builder().year(2006).vin("1234").costInUSD(10_000L).make("Nissan").model("Sentra").build();
        dealershipService.buy(car);
        assertThat(dealershipService.getRevenue(), is(990_000L));
    }
}
  • 注意:每次刷新上下文会增加测试耗时,适合测试方法较少的场景。

2. 测试中手动创建实例

不依赖Spring注入,在每个测试方法中手动实例化DealershipServiceImpl,同时传入测试用的CarRepository(可通过@MockBean模拟)。

修改后的测试代码(建议给实现类添加带初始revenue的构造方法):

@SpringBootTest(classes = TestConfig.class)
public class DealershipServiceIT {

    @MockBean
    private CarRepository carRepository;

    @Value("${revenue}")
    private Long initialRevenue;

    @Test
    public void whenNoTransactions_TotalRevenueIsAMil(){
        // 假设实现类新增构造方法:public DealershipServiceImpl(CarRepository carRepository, Long revenue)
        DealershipService service = new DealershipServiceImpl(carRepository, initialRevenue);
        assertThat(service.getRevenue(), is(1_000_000L));
    }

    @Test
    public void whenMakingTransactions_revenueIsAffected(){
        DealershipService service = new DealershipServiceImpl(carRepository, initialRevenue);
        Car car = Car.builder().year(2006).vin("1234").costInUSD(10_000L).make("Nissan").model("Sentra").build();
        service.buy(car);
        assertThat(service.getRevenue(), is(990_000L));
    }
}
  • 优点:完全隔离测试实例,不依赖Spring上下文状态,测试执行更快。

3. 测试前重置Bean状态(推荐)

在每个测试方法执行前,重置dealershipService的revenue字段和CarRepository的状态,保证初始一致。

修改后的测试代码:

@SpringBootTest(classes = TestConfig.class)
public class DealershipServiceIT {

    @Autowired
    private DealershipService dealershipService;

    @Value("${revenue}")
    private Long initialRevenue;

    @BeforeEach
    void setUp() throws NoSuchFieldException, IllegalAccessException {
        // 反射重置revenue字段
        Field revenueField = DealershipServiceImpl.class.getDeclaredField("revenue");
        revenueField.setAccessible(true);
        revenueField.set(dealershipService, initialRevenue);
        
        // 重置CarRepository状态(假设仓库有清空方法)
        ((DealershipServiceImpl)dealershipService).getCarRepository().clear();
    }

    @Test
    public void whenNoTransactions_TotalRevenueIsAMil(){
        assertThat(dealershipService.getRevenue(), is(1_000_000L));
    }

    @Test
    public void whenMakingTransactions_revenueIsAffected(){
        Car car = Car.builder().year(2006).vin("1234").costInUSD(10_000L).make("Nissan").model("Sentra").build();
        dealershipService.buy(car);
        assertThat(dealershipService.getRevenue(), is(990_000L));
    }
}
  • 优点:无需修改Spring上下文,测试性能好,同时保证每个测试的初始状态一致。

4. 测试配置中覆盖Bean Scope

创建测试专用的配置类,覆盖DealershipService的Bean定义,设置为prototype scope,仅在测试环境生效。

测试配置类:

@TestConfiguration
public class TestDealershipConfig {

    @Bean
    @Scope("prototype")
    public DealershipService dealershipService(CarRepository carRepository, @Value("${revenue}") Long revenue) {
        DealershipServiceImpl service = new DealershipServiceImpl(carRepository);
        // 手动设置初始revenue
        service.setRevenue(revenue);
        return service;
    }
}

修改测试类:

@SpringBootTest(classes = {TestConfig.class, TestDealershipConfig.class})
public class DealershipServiceIT {

    @Autowired
    private DealershipService dealershipService;

    // 测试方法不变
}
  • 优点:不修改生产代码,仅在测试中改变Bean的Scope,适合需要复用测试配置的场景。

内容的提问来源于stack exchange,提问作者Jason

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 00:55:02