테스트 코드를 만들다가 TestContext에서 트랜잭션 관리는 ApplicationContext에서 관리하는 방식이나 애플리케이션 코드 내에서 관리하고 있는 트랜잭션과 별개라는 것을 알게 됐다. 이에 대한 자세한 내용은 아래 링크를 참고하자.

Transaction Management :: Spring Framework

아무튼 겪었던 문제는 @Test 메서드에서 @Transactional을 쓰지 않으려고 했던 것부터 시작한다. @Test 메서드에서 @Transactional 어노테이션을 달게 되면 정확한 테스트가 되지 않을 가능성이 있다. 예를 들면, 검증하고 싶은 서비스 계층의 메서드에 @Transactional을 실수로 빠뜨렸다고 가정해보자. 실제로 애플리케이션이 실행된다면 이는 의도치 않은 기능으로 작동하거나 오류가 발생할 것이다. 그러나 @Test 메서드에 @Transactional을 쓰게 되면 테스트는 정상 통과되고 해당 기능은 문제가 없을 것이라고 판단하게 된다. 따라서 테스트 메서드에서는 @Transactional을 쓰는 것을 이 점을 유의하여 사용해야 한다.

JPA 사용시 테스트 코드에서 @Transactional 주의하기

한편, 테스트 코드에서 하나의 테스트가 끝나고 나서 데이터를 rollback해야 정상적인 테스트를 진행할 수 있었다. 그래서 @Test 메서드에 @Transactional을 쓰지 않은 상태에서 @AfterEach 메서드에 @Transactional을 쓰면 원하는 rollback 로직이 동작할 것이라 기대하였다.

//...

@SpringBootTest
class FacilityServiceTest {

    private Facility facility;

    @Autowired
    private FacilityService facilityService;
    @Autowired
		private EntityManager em;
		//...

    @BeforeEach
    void initFacility() {
        authorize();

        FacilityDto facilityDto = FacilityDto.builder()
                .address(Address.builder()
                        .jibunAddress("지번 주소")
                        .roadAddress("도로명 주소")
                        .latitude(0D)
                        .longitude(0D)
                        .build())
                .category(FacilityType.HEALTH)
                .build();
        facility = facilityService.createFacility(facilityDto);
    }

		@Transactional
    @AfterEach
    void rollback() {
				em.createQuery("delete from Facility f where (f.title not in :title) or (f.title is null)")
				                .setParameter("title", List.of(TestInit.FACILITY_TITLE))
				                .executeUpdate();
    }

    @Test
    @DisplayName("Update Dto를 통해 facility 정보를 수정한다.")
    void updateFacility() {
        Facility findFacility = facilityService.findFacility(facility.getId());
        FacilityDto updateDto = FacilityDto.builder()
                .id(findFacility.getId())
                .address(Address.builder()
                        .jibunAddress("변경된 주소")
                        .latitude(0D)
                        .longitude(0D)
                        .build())
                .category(FacilityType.HEALTH)
                .build();
        facilityService.updateFacility(updateDto);

        Facility updateFacility = facilityService.findFacility(findFacility.getId());
        assertThat(updateFacility.getAddress().getJibunAddress()).isEqualTo("변경된 주소");
    }

    //...

}

하지만 이 코드는 정상적으로 rollback이 되지 않았고 원인을 찾아 보다가 TestContext의 트랜잭션 관리에 대해 알게 되었다. 구체적으로 말하자면 @Test 메서드에서의 트랜잭션은 TransactionalTestExecutionListener에 의해 관리되는데, @BeforeEach나 @AfterEach 메서드의 callback은 @Test 메서드에서 정의된 트랜잭션 안에서 이루어진다. 그래서 @BeforeEach나 @AfterEach 메서드에@Transactional을 쓰는 것은 무용지물이다. 따라서 @Test 메서드에 @Transactional을 쓰지 않으면 @AfterEach를 쓰고 있는 rollback() 메서드에서는 존재하는 트랜잭션이 없기 때문에 DB에 쿼리가 반영되지 않는다.

@Before and @Transactional

그래서 해결 방법으로서 아래와 같은 방법을 선택했다. rollback() 메서드를 다른 클래스로 분리하여 @Transactional이 온전하게 동작하도록 만드는 것이다. TestDataProcessor를 Spring Bean으로 등록하고 테스트 클래스 어디에서나 가져다 쓸 수 있도록 하는 방법이다.

//...

@Component
public class TestDataProcessor {

    @Autowired
    private EntityManager em;

    public TestDataProcessor(EntityManager em) {
        this.em = em;
    }

    @Transactional
    public void rollback() {
        em.createQuery("delete from Facility f where (f.title not in :title) or (f.title is null)")
                .setParameter("title", List.of(TestInit.FACILITY_TITLE))
                .executeUpdate();
    }
}
//...

@SpringBootTest
class FacilityServiceTest {

    private Facility facility;

    @Autowired
    private FacilityService facilityService;
    @Autowired
    private TestDataProcessor processor;
			//...

    @BeforeEach
    void initFacility() {
        //...
    }

    @AfterEach
    void rollback() {
        processor.rollback();
    }

    @Test
    @DisplayName("Update Dto를 통해 facility 정보를 수정한다.")
    void updateFacility() {
        Facility findFacility = facilityService.findFacility(facility.getId());
        FacilityDto updateDto = FacilityDto.builder()
                .id(findFacility.getId())
                .address(Address.builder()
                        .jibunAddress("변경된 주소")
                        .latitude(0D)
                        .longitude(0D)
                        .build())
                .category(FacilityType.HEALTH)
                .build();
        facilityService.updateFacility(updateDto);

        Facility updateFacility = facilityService.findFacility(findFacility.getId());
        assertThat(updateFacility.getAddress().getJibunAddress()).isEqualTo("변경된 주소");
    }

    //...
}