테스트 코드를 만들다가 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에 쿼리가 반영되지 않는다.
그래서 해결 방법으로서 아래와 같은 방법을 선택했다. 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("변경된 주소");
}
//...
}