이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
@SpringBootTest,@DataJpaTest, Testcontainers는 각각 어떤 수준의 검증을 제공하는가?- 테스트 DB를 H2로 둘지 실제 DB 컨테이너로 둘지는 어떤 기준으로 선택하는가?
- 통합 테스트에서 transaction rollback, test data, context cache를 어떻게 관리해야 하는가?
개요
통합 테스트는 여러 구성요소가 함께 맞물리는지 확인한다. Service, Repository, TransactionManager, DataSource, Security FilterChain, Jackson, Validation, AutoConfiguration이 실제 설정과 함께 동작하는지를 본다.
다만 통합 테스트는 비용이 크다. 느리고, 실패 원인이 넓고, 외부 인프라가 필요할 수 있다. 그래서 모든 테스트를 통합 테스트로 만들기보다 핵심 경로와 위험한 인프라 계약에 집중해야 한다.
SpringBootTest
@SpringBootTest는 Boot 애플리케이션의 넓은 context를 로드한다. webEnvironment 설정에 따라 mock web environment, random port 실제 서버 환경 등을 선택할 수 있다.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderApiIntegrationTest {
@Autowired TestRestTemplate restTemplate;
@Test
void placeOrder() {
ResponseEntity<OrderResponse> response = restTemplate.postForEntity(
"/api/orders",
new OrderRequest(1L, 2),
OrderResponse.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
}
}이 테스트는 실제 HTTP stack에 더 가깝다. 대신 속도와 디버깅 비용이 커지므로 대표 사용자 흐름에 집중한다.
RANDOM_PORT나 DEFINED_PORT처럼 실제 서버를 띄우는 테스트는 테스트 메서드와 서버 요청 처리가 서로 다른 thread에서 실행된다. 그래서 테스트 메서드의 transaction rollback이 서버 내부에서 commit된 데이터를 자동으로 되돌린다고 기대하면 안 된다.
DataJpaTest
@DataJpaTest는 JPA 관련 slice를 로드한다. Entity mapping, Repository query, transaction, JPA provider 동작을 검증하는 데 적합하다.
@DataJpaTest
class MemberRepositoryTest {
@Autowired MemberRepository memberRepository;
@Test
void findByEmail() {
memberRepository.save(new Member("a@b.com"));
Optional<Member> result = memberRepository.findByEmail("a@b.com");
assertThat(result).isPresent();
}
}이 테스트는 Service나 Controller를 보지 않는다. 대신 Repository 쿼리와 mapping 문제를 빠르게 잡는다.
Boot의 Data JPA 테스트는 기본적으로 transactional이며 각 테스트가 끝나면 rollback된다. 빠른 repository 검증에는 유리하지만, 운영 DB와 다른 embedded database로 대체되면 dialect 차이를 놓칠 수 있다. 실제 DB 컨테이너를 붙이는 경우에는 test database replacement 설정과 datasource 연결 방식을 함께 확인한다.
Testcontainers
H2는 빠르고 가볍지만 운영 DB와 다른 부분이 있다. PostgreSQL, MySQL, MariaDB의 dialect, index, lock, constraint, JSON type, timezone, sequence 동작은 H2와 다를 수 있다.
Testcontainers는 테스트 중 실제 DB 컨테이너를 띄워 운영 DB와 가까운 검증을 가능하게 한다. Spring Boot는 Testcontainers와 통합을 제공하므로 datasource property를 동적으로 연결할 수 있다.
@Testcontainers
@SpringBootTest
class OrderRepositoryContainerTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
}@ServiceConnection은 지원되는 Testcontainers 타입의 연결 정보를 Boot 자동 설정에 전달한다. 지원되지 않는 컨테이너나 더 세밀한 property 제어가 필요할 때는 @DynamicPropertySource로 datasource URL, username, password를 직접 등록한다.
데이터 격리
Spring 테스트의 트랜잭션 rollback은 편리하다. 하지만 비동기 작업, 별도 transaction propagation, 실제 HTTP 서버 호출, container reuse에서는 단순 rollback만으로 격리가 완전하지 않을 수 있다.
테스트 데이터는 fixture builder, SQL script, factory method 등으로 일관되게 준비한다. 각 테스트가 서로의 데이터에 의존하지 않게 해야 한다. sequence 값이나 시간 의존성도 테스트 안정성에 영향을 준다.
실전 팁
- Repository 쿼리는
@DataJpaTest로 빠르게 검증한다. - 운영 DB 의존 동작은 Testcontainers로 검증한다.
- Testcontainers와 JPA slice를 함께 쓸 때는 embedded database로 교체되는지 확인한다.
- 전체 API 흐름은 대표 happy path와 중요한 실패 path만
@SpringBootTest로 둔다. - 통합 테스트에서도 context 설정 조합을 줄여 cache를 살린다.
- 테스트 데이터는 각 테스트가 명시적으로 준비하고 서로 공유하지 않는다.
위험 신호!
- H2에서만 통과하고 운영 DB에서 깨지는 쿼리가 반복된다.
- 통합 테스트가 수백 개로 늘어 PR 피드백이 너무 늦다.
- 테스트끼리 데이터 순서와 sequence 값에 의존한다.
- 컨테이너 시작 비용 때문에 테스트를 잘 실행하지 않게 된다.
- 실제 HTTP 서버 테스트에서 transaction rollback을 기대한다.
확인 질문
확인 질문
@DataJpaTest가 잘 맞는 대상은 무엇인가?
- Entity mapping, Repository query, JPA slice 동작 검증이다.
- Testcontainers를 사용하는 주된 이유는 무엇인가?
- 운영 인프라와 같은 종류의 DB나 외부 시스템으로 더 현실적인 통합 검증을 하기 위해서다.
- 실제 서버를 띄우는 통합 테스트에서 rollback을 조심해야 하는 이유는 무엇인가?
- 테스트 메서드 트랜잭션과 서버 요청 처리 트랜잭션이 다를 수 있어 단순 rollback으로 데이터가 정리되지 않을 수 있기 때문이다.