Beware of @DirtiesContext

  • @DirtiesContext is a Spring testing annotation.
  • It tells spring that the Class or test method with this annotation modifies the application context.
  • Once @DirtiesContext is used, the cached application context will be recreated.
  • @DirtiesContext is both a class and method level annotation.

The problems

Cost and speed of unit tests

Closing and recreating the application context is costly. Imagine a big project with a lot of classes and tests where we are using @DirtiesContext a lot. This will greatly slow the testing of the application.

False Negative tests

These are tests that should fail but are successfully running. Consider the below example which demonstrate a transaction error.

// Service interface
public interface ThemeParkRideService {
    /**
     * Find all rides
     *
     * @return list of ThemeParkRide
     */
    List<ThemeParkRide> getRides();

    /**
     * Create ride
     *
     * @param themeParkRide
     */
    void createRide(ThemeParkRide themeParkRide);
}

In the service implementation below, in the method createRide(themeParkRide), see the part throw new RuntimeException("Example error"). Here we are simulating a runtime error. The service also do not have an @Transactional annotation set.

// Service implementation
@RequiredArgsConstructor
@Service
public class ThemeParkRideServiceImpl implements ThemeParkRideService {
    
    private final ThemeParkRideRepository themeParkRideRepository;

    @Override
    public List<ThemeParkRide> getRides() {
        return themeParkRideRepository.findAll();
    }

    @Override
    public void createRide(ThemeParkRide themeParkRide) {
        themeParkRideRepository.save(themeParkRide);
        throw new RuntimeException("Example error");
    }
}
// Test class
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class ThemeParkRideServiceBadTest {

    @Autowired
    private ThemeParkRideService service;
    @Autowired
    private ThemeParkRideRepository repository;

    @Test
    @Order(1)
    @DirtiesContext
    void testCreateRide() {
        ThemeParkRide ride = new ThemeParkRide("Rollercoaster",
                "Train ride",
                5,
                3);
        Assertions.assertThrows(RuntimeException.class, () ->
                service.createRide(ride));
    }

    @Test
    @Order(2)
    void testGetRides() {
        repository.save(
                new ThemeParkRide("Teacups",
                        "Spinning ride",
                        2,
                        4));
        List<ThemeParkRide> rideList = service.getRides();
        assertThat("One ride must be available", rideList, hasSize(1));
        assertThat("Name is Teacups", rideList.get(0).getName(), is("Teacups"));
    }
}

Here all the tests will run successfully as the testCreateRide() will get the expected RuntimeException and the testGetRides() will get one ThemeParkRide with name “Teacups”.

These are clearly a false negative tests as the @DirtiesContext annotation is hiding the transaction problem in the service implementation class.

The solution

Remove @DirtiesContext from the test method testCreateRide().

// Refactored test class
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class ThemeParkRideServiceBadTest {

    @Autowired
    private ThemeParkRideService service;
    @Autowired
    private ThemeParkRideRepository repository;

    /**
     * Test method with a @DirtiesContext annotation to clean all created data after the test.
     * Test is expecting a RuntimeException
     */
    @Test
    @Order(1)
    void testCreateRide() {
        ThemeParkRide ride = new ThemeParkRide("Rollercoaster",
                "Train ride",
                5,
                3);
        Assertions.assertThrows(RuntimeException.class, () ->
                service.createRide(ride));
    }

    /**
     * Test method is checking data fetch
     */
    @Test
    @Order(2)
    void testGetRides() {
        repository.save(
                new ThemeParkRide("Teacups",
                        "Spinning ride",
                        2,
                        4));
        List<ThemeParkRide> rideList = service.getRides();
        assertThat("One ride must be available", rideList, hasSize(1));
        assertThat("Name is Teacups", rideList.get(0).getName(), is("Teacups"));
    }
}

Here only the test testCreateRide() will run successfully get the expected RuntimeException however the test testGetRides() will now fail as it is expecting one ThemeParkRide with name “Teacups” but instead is getting two rides. One with name “Teacups” and the other with name “Teacups”

The explanation

  • Spring rollbacks transactions on RuntimeException detection.
  • Since we did not use an @Transactional annotation in our service class, createRide() method will commit even though it encounters a RuntimeException.
  • The test method testGetRides() is now failing as the it is getting the committed data from createRide() also.
  • This is a correct behavior which alerts the developer that an @Transactional should be set in the service class.
  • @DirtiesContext was silently hiding the transactional problem.

Leave a Reply