如何在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
相关产品推荐
相关产品推荐

