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 DealershipService
a prototype the only possibility?
Your tests are reusing the application context, to make them independent from one another you should use @DirtiesContext
annotation:
@DirtiesContext
indicates that the underlying SpringApplicationContext
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 {
//..
}