현재 우리팀은 매 테스트마다 상관관계를 미치지 않기 위해, @Transactional 어노테이션을 이용하여 실제로 db에 저장하지 않고 롤백하는 방법을 채택했다.
하지만
이런 이유 때문에, 테스트에서 @Transactional을 제거하자고 결정을 내렸고,
Transactional 없이 테스트를 격리할 수 있는 방법을 찾아보았다.
(출처 : https://tecoble.techcourse.co.kr/post/2020-09-15-test-isolation/)
3개의 선택지 중에서 제일 나아보이는 2번을 선택하였다.
(사실 위 방법을 제외하고도 테스트 격리를 하는 방법이 더 있을수도 있지만, 곧 프로젝트 마감이므로 빠른 선택이 필요할 것 같아 위 방법들에서만 선택지를 고려하였다.)
그런데 왜 이런 선택을 내린걸까 ?
위의 적용 계기의 2번에서 연장해서 설명해 보겠다.
// test fixture 세팅
@BeforeEach
void setUp() {
delivery = deliverySetUp.saveOne(1L);
rider = riderSetUp.saveOne();
}
// 테스트 코드
@Transactional
@Nested
@DisplayName("배달기사를 배정할 수 있다.")
class allocateRider {
@Test
@DisplayName("성공한다.")
void success_test() {
// when
DeliveryHistoryResponse deliveryHistoryResponse =
deliveryService.allocateRider(delivery.getDeliveryId(),rider.getRiderId());
// then
List<DeliveryHistory> deliveryHistories = deliveryHistoryRepository
.findDeliveryHistoriesByDeliveryId(delivery.getDeliveryId());
assertThat(deliveryHistories).hasSize(2);
assertThat(delivery.getRider().getRiderId()).isEqualTo(rider.getRiderId());
assertThat(deliveryHistoryResponse.deliveryStatus()).isEqualTo(DeliveryStatus.ALLOCATED);
}
}
// 서비스 코드
@Transactional
public DeliveryHistoryResponse allocateRider(
Long deliveryId,
Long riderId
) {
Delivery delivery = deliveryRepository.findById(deliveryId)
.orElseThrow(
() -> new EntityNotFoundException(ErrorCode.DELIVERY_NOT_FOUND)
);
Rider rider = riderRepository.findById(riderId)
.orElseThrow(
() -> new EntityNotFoundException(ErrorCode.DELIVERY_NOT_FOUND)
);
delivery.attach(rider);
DeliveryHistory deliveryHistory = DeliveryHistory.createAllocatedDeliveryHistory(delivery);
DeliveryHistory savedDeliveryHistory = deliveryHistoryRepository.save(deliveryHistory);
DeliveryHistoryResponse response = DeliveryHistoryResponse.of(
rider,
savedDeliveryHistory
);
return response;
}
배달 기사 배정 테스트코드이다. deliveryService의 allocateRider메소드를 호출한다.(이때, delivery와 rider는 이미 영속화된 상태이다.)
기존의 @Transactional을 적용했을 땐 정상작동되지만, @Transactional 어노테이션을 삭제한 후,
@Sql을 붙여서 매 테스트마다 롤백이 아닌 DB를 truncate해주면 테스트가 실패한다. 아래는 테스트 실패 메시지이다.
분명 deliveryService의 allocateRider에서, delivery객체와 rider를 연관관계 매핑 해주었다. (delivery.attach(rider)
가 그 코드이다.)
디버깅 했을때도 연관관계 매핑은 문제가 없었지만, 문제는 서비스 메소드가 끝나고, 나머지 테스트코드를 수행할 때 이다.
현재 테스트코드에는 Transactional이 붙어있지 않아, 서비스 메소드가 수행 된 이후에는 영속성 컨텍스트가 없어져 버린다. 따라서 테스트 코드로 돌아왔을 때, DB상으론 연관관계 매핑이 되어있지만(FK로 delivery)delivery객체 자체는 연관관계 매핑이 되어있지 않은 상태이다.
allocateRider호출 이전
allocateRider호출 이후. DB에는 rider_id가 FK로 들어왔지만, 밑의 delivery객체의 rider는 null이다.
따라서 테스트 코드 자체가 잘못되었다 판단하여, 아래의 코드로 수정하였다.
// truncate.sql
SET foreign_key_checks = 0;
TRUNCATE TABLE addresses;
TRUNCATE TABLE deliveries;
TRUNCATE TABLE delivery_histories;
TRUNCATE TABLE members;
TRUNCATE TABLE menu_option_group;
TRUNCATE TABLE menu_options;
TRUNCATE TABLE menus;
TRUNCATE TABLE order_histories;
TRUNCATE TABLE order_items;
TRUNCATE TABLE orders;
TRUNCATE TABLE riders;
TRUNCATE TABLE selected_options;
TRUNCATE TABLE shops;
SET foreign_key_checks = 1;
@Sql("/truncate.sql")
@Nested
@DisplayName("배달기사를 배정할 수 있다.")
class allocateRider {
@Test
@DisplayName("성공한다.")
void success_test() {
// when
DeliveryHistoryResponse deliveryHistoryResponse = deliveryService
.allocateRider(delivery.getDeliveryId(),rider.getRiderId());
// then
// 연관관계가 매핑된, 최신의 데이터와 비교하기 위해 DB에서 find
Delivery savedDelivery = deliveryRepository.findById(delivery.getDeliveryId()).get();
List<DeliveryHistory> deliveryHistories = deliveryHistoryRepository
.findDeliveryHistoriesByDeliveryId(delivery.getDeliveryId());
assertThat(deliveryHistories).hasSize(2);
assertThat(savedDelivery.getRider().getRiderId()).isEqualTo(rider.getRiderId());
assertThat(deliveryHistoryResponse.deliveryStatus()).isEqualTo(DeliveryStatus.ALLOCATED);
}
}
현재 있는 선택지들 중에 그나마 낫다고 생각하는 것 해결책을 도입했는데, 이마저도 편리한 방법까지는 아닌 듯 싶다. 실무에서는 테스트를 어떻게 진행할까 궁금해지는 계기가 되었다.