javaspringspring-bootspring-boot-testprototype-scope

How to make a SpringBootTest treat the autowired instance as a prototype?


New to Spring / SpringBoot. I have this interface that represents a toy car dealership:


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);
}

And its implementation, for which only the buy() and sell() methods are really the only relevant ones:

@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();
    }
  }
.
.
.

Buying a car reduces the available revenue, and selling one increases it. The value of revenue is at 1_000_000L (one million USD, supposedly) in my application.properties files (under both main and test).

In trying to test this implementation, I have written the following:

@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));
    }
}

The problem is that the action of whenMakingTransactions_revenueIsAffected() affects the singleton field dealershipService injected into the class. Consequently, when I run all the tests of the class and this test is run first (I don't know why the tests aren't run in the order written), this test succeeds and whenNoTransactions_TotalRevenueIsAMil() fails. This is because the revenue falls by 10k and then the first test sees the same singleton instance of dealershipService.

This is solved if I make my DealershipServiceImpl a prototype bean by adding @Scope("prototype")above DealershipServiceImpl, but I'm not quite certain that this is good Spring design; there's a reason why Beans are singletons by default.

So I'm looking to find a way to treat the injected DealershipService in my test as a prototype for the purposes of testing, if at all possible. Effectively, whenever entering a new test, I'd need a new DealershipService being injected into the test. Is making my original DealershipServicea prototype the only possibility?


Solution

  • Your tests are reusing the application context, to make them independent from one another you should use @DirtiesContext annotation:

    @DirtiesContext indicates that the underlying Spring ApplicationContext has been dirtied during the execution of a test (that is, the test modified or corrupted it in some manner — for example, by changing the state of a singleton bean) and should be closed. When an application context is marked as dirty, it is removed from the testing framework’s cache and closed. As a consequence, the underlying Spring container is rebuilt for any subsequent test that requires a context with the same configuration metadata.

    For example, you can annotate the test class as follows:

    @SpringBootTest(classes =  TestConfig.class)
    @DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
    public class DealershipServiceIT {
    //..
    }