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?
Your tests are reusing the application context, to make them independent from one another you should use
@DirtiesContextannotation:For example, you can annotate the test class as follows: