JPA를 활용한 Repository 테스트를 작성할 때, 실제 데이터베이스와 상호작용하는 테스트가 필요할 때가 많습니다. 이를 위해 H2와 같은 인메모리 데이터베이스를 사용하여 테스트 환경을 구축할 수 있습니다. 이때, @SpringBootTest와 같은 전체 애플리케이션 컨텍스트를 로딩하는 대신, @DataJpaTest를 사용하여 JPA 관련 컴포넌트만 로드하는 방법이 효과적입니다. 또한, @Transactional을 사용하여 테스트 후 데이터베이스 상태가 롤백되도록 보장할 수 있습니다.
이 포스트에서 다루는 문제 해결은 다음과 같습니다. (트러블 슈팅 경험)
- DataJpaTest이용시 의존받는 일부 bean를 로드하지 못하는 문제 해결
- @Autowired 어노테이션을 이용시 해당 인스턴스의 상태공유가 안되는 문제
@DataJpaTest란?
@DataJpaTest는 Spring Data JPA 관련 컴포넌트만 로딩하여 테스트하는 어노테이션입니다. 이 어노테이션은 실제 데이터베이스와 상호작용하는 테스트를 빠르게 진행할 수 있도록 해주며, 데이터베이스 설정을 최소화하고 JPA와 관련된 기능만을 테스트할 수 있도록 도와줍니다.
주요 특징:
- JPA 관련 컴포넌트만 로딩: 데이터베이스와 관련된 기능(예: 엔티티 매핑, 리포지토리 동작)을 테스트할 수 있습니다.
- • 인메모리 데이터베이스 사용: 보통 H2와 같은 인메모리 데이터베이스를 사용하여 테스트합니다.
- 테스트 데이터 롤백: 기본적으로 @Transactional이 활성화되어 테스트가 끝난 후 데이터베이스 상태가 롤백됩니다. 이를 통해 테스트 데이터가 실제 DB에 영향을 미치지 않도록 보장할 수 있습니다.
@Transactional이란?
@Transactional 어노테이션은 테스트 메서드에 적용하여 해당 메서드에서 발생한 모든 DB 작업을 하나의 트랜잭션으로 묶고, 테스트 종료 후 롤백되도록 합니다. 이로 인해 테스트 데이터가 실제 데이터베이스에 반영되지 않고, 테스트 후 데이터가 DB에 영향을 미치지 않도록 할 수 있습니다.
주요 특징:
- 트랜잭션 롤백: 테스트가 끝난 후 트랜잭션을 롤백하여 DB 상태를 원래대로 되돌립니다.
- 테스트 데이터 격리: 여러 테스트가 독립적으로 실행될 수 있도록 보장합니다.
@DataJpaTest는 기본적으로 @Transactional을 활성화하므로, 별도로 명시하지 않아도 테스트 후 롤백이 적용됩니다. 하지만, 이를 명확하게 보장하려면 @Transactional을 명시적으로 추가하는 것이 좋습니다.
예시코드
@DataJpaTest
@Transactional
public class TourRepositoryTest {
@Autowired
private TourRepository tourRepository;
@Test
public void testSaveAndFindTour() {
// given
Tour tour = new Tour("Paris", "A beautiful city");
// when
tourRepository.save(tour);
Tour foundTour = tourRepository.findById(tour.getId()).orElse(null);
// then
assertNotNull(foundTour);
assertEquals(tour.getName(), foundTour.getName());
}
}
이 예시에서는 @DataJpaTest와 @Transactional을 사용하여, TourRepository를 테스트하고 있습니다. 테스트가 끝난 후, H2 데이터베이스에서 변경된 내용은 롤백되므로 실제 데이터베이스에 영향이 가지 않습니다.
@DataJpaTest와 @Transactional을 활용한 Repository 테스트는 JPA와 관련된 기능을 독립적으로 빠르게 테스트할 수 있도록 해주며, 테스트 데이터의 롤백을 보장하여 DB에 영향을 미치지 않도록 합니다. 이를 통해 테스트 환경을 분리하고, 테스트의 신뢰성을 높일 수 있습니다.
DataJpaTest시 의존받는 bean를 로드하지 못하는 문제 해결
아래 어노테이션 추가
@ContextConfiguration(classes = ProducerApplication.class)
- 목적: Spring의 ApplicationContext를 설정할 때 특정 클래스들을 로드하도록 지정합니다.
- 설명: 기본적으로 @DataJpaTest는 JPA 관련 설정만 로드하지만, ProducerApplication을 명시하여 ProducerApplication 클래스에서 정의한 빈들도 로드되게 합니다. 이는 필요할 경우 애플리케이션의 특정 설정이나 컴포넌트를 테스트에 포함시킬 수 있게 합니다.
@Import({FilterRepository.class, LocationLabeler.class})
• 목적: 테스트 클래스에서 사용할 특정 빈을 명시적으로 불러옵니다.
• 설명: @Import는 테스트 클래스에서 사용하고자 하는 다른 빈들을 불러올 수 있게 해줍니다.
@DataJpaTest
@ContextConfiguration(classes = ProducerApplication.class)
@EntityScan(basePackages = "entity")
@Import({FilterRepository.class, LocationLabeler.class})
@Transactional
public class FilterRepositoryTest {
@Autowired
private EntityManager entityManager;
@Autowired
private FilterRepository filterRepository;
@Autowired
private LocationLabeler locationLabeler;
/// ..
}
이 예시에서는 FilterRepository와 LocationLabeler를 테스트에 포함시키기 위해 이 어노테이션을 사용하고 있습니다
@Import를 사용하면, 테스트 컨텍스트에 필요한 클래스를 명시적으로 추가할 수 있어 의존성을 주입하거나 사용할 수 있게 됩니다.
@ContextConfiguration은 ProducerApplication 클래스에서 정의된 빈과 설정들을 전반적으로 로드하지만, 테스트에서 사용해야 하는 모든 빈을 자동으로 로드하지는 않습니다.
@Import는 애플리케이션의 기본 설정 외에 특정 빈들만 따로 추가할 때 사용됩니다. FilterRepository와 LocationLabeler는 기본 설정에서 자동으로 로드되지 않거나 테스트에서 명시적으로 필요한 빈들이기 때문에 @Import를 통해 테스트 환경에 추가됩니다.
Test에서 @Autowired 어노테이션을 이용시 인스턴스의 상태공유가 안되는 문제 해결
원인
JUnit 5의 기본 동작은 각 테스트 메서드 실행 시 테스트 클래스의 새로운 인스턴스를 생성하는 것입니다. 이로 인해 클래스 필드에 선언된 상태가 테스트 메서드 간에 공유되지 않고 초기화됩니다. 테스트 메서드가 같은 객체의 상태를 공유해야 하는 경우, 이러한 동작이 문제를 일으킬 수 있습니다. 특히, @Autowired로 주입된 객체가 클래스의 상태에 의존하는 경우, 각 테스트 메서드에서 별도의 인스턴스가 생성되어 기대한 동작이 이루어지지 않을 수 있습니다.
해결
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
- 목적: 테스트 클래스 인스턴스를 모든 테스트 메서드에서 하나만 생성하도록 설정합니다.
- 설명: 기본적으로 JUnit 5는 각 테스트 메서드 실행 시마다 테스트 클래스 인스턴스를 새로 생성합니다. 그러나 PER_CLASS를 사용하면 클래스 인스턴스를 한 번만 생성하고, 각 테스트 메서드는 그 인스턴스를 공유하게 됩니다. 이 설정을 사용하면, 클래스 필드에 선언된 상태를 테스트 간에 공유할 수 있어 성능을 개선할 수 있습니다. 예를 들어, @BeforeEach에서 초기화된 값들이 테스트 간에 지속될 수 있습니다.
@DataJpaTest
@ContextConfiguration(classes = ProducerApplication.class)
@EntityScan(basePackages = "entity")
@Import({FilterRepository.class, LocationLabeler.class})
@Transactional
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class FilterRepositoryTest {
@Autowired
private EntityManager entityManager;
@Autowired
private FilterRepository filterRepository;
@Autowired
private LocationLabeler locationLabeler;
'Server > 🌱 Spring Boot (java)' 카테고리의 다른 글
[Spring boot jpa] 찜/좋아요 삭제시 연관된 엔티티도 같이 삭제되는 문제 해결 (0) | 2025.01.25 |
---|---|
[Spring boot] SQL 예약어와 테이블 이름 충돌 문제 해결 (0) | 2025.01.25 |
[Spring boot] @Scheduled 어노테이션을 사용한 스케줄링이 작동하지 않는 문제 해결 (0) | 2025.01.25 |
[Spring Boot] 멀티-모듈 프로젝트 build시 common 모듈의 의존성 받지 못하는 문제 해결 (0) | 2025.01.24 |
[Gradle] common을 사용하는 멀티-모듈 프로젝트에서 순환참조 문제 해결 및 최적화 (0) | 2025.01.24 |
댓글