이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- Spring 테스트에서 단위 테스트, slice 테스트, 통합 테스트를 어떻게 나누어야 하는가?
- TestContext Framework와 context caching은 테스트 속도에 어떤 영향을 주는가?
- 느린 Spring 테스트 스위트를 빠르게 만들 때 무엇부터 확인해야 하는가?
개요
Spring 애플리케이션 테스트는 한 가지 방식으로 끝나지 않는다. 도메인 규칙은 순수 단위 테스트로 빠르게 검증하고, MVC나 JPA 같은 계층은 slice 테스트로 좁게 검증하며, 실제 Bean wiring과 인프라 흐름은 통합 테스트로 확인한다.
Spring TestContext Framework는 Spring 테스트의 기반이다. 테스트용 ApplicationContext 로딩, 의존성 주입, 트랜잭션 테스트, context caching 같은 기능을 제공한다. 이 기능을 이해하면 테스트가 왜 느려지는지도 보인다.
테스트 계층
단위 테스트는 Spring Context를 띄우지 않는다. 객체를 직접 생성하고 필요한 협력 객체를 fake나 mock으로 대체한다. 빠르고 실패 원인이 좁다.
Slice 테스트는 특정 계층만 로드한다. @WebMvcTest는 MVC 계층, @DataJpaTest는 JPA 계층, JSON 테스트는 직렬화 계층에 집중한다. 실제 Spring 인프라 일부를 쓰면서 범위를 줄인다.
통합 테스트는 여러 계층이 함께 동작하는지 확인한다. @SpringBootTest는 넓은 context를 로드하므로 실제와 가깝지만 느리고 실패 원인이 넓다.
TestContext와 캐싱
Spring TestContext는 한 번 로드한 ApplicationContext를 캐싱한다. 같은 설정 조합의 테스트가 이어지면 context를 재사용해 속도가 빨라진다.
문제는 설정 조합이 많아질 때다. TestContext는 configuration class, initializer, @ActiveProfiles, @TestPropertySource, context customizer, @MockitoBean 같은 Bean override 정보를 조합해 cache key를 만든다. 이 조합이 테스트마다 다르면 context를 계속 새로 띄운다.
느린 테스트 스위트는 대개 이 조합이 불필요하게 많다. @DirtiesContext는 context를 cache에서 제거하므로 정말 context가 오염된 경우에만 사용한다. 원인을 볼 때는 org.springframework.test.context.cache 로그를 켜면 캐시 hit/miss를 확인할 수 있다.
트랜잭션 테스트
TestContext가 관리하는 트랜잭션 테스트는 기본적으로 테스트 종료 후 rollback된다. Repository나 Service가 같은 thread의 test-managed transaction에 참여하면 데이터 정리가 편하다.
하지만 preemptive timeout, 비동기 작업, 실제 서버를 띄운 HTTP 테스트처럼 실행 thread가 달라지는 경우에는 같은 rollback 경계라고 볼 수 없다. 통합 테스트에서 데이터가 남는다면 @Transactional이 붙어 있는지보다 실제 실행 경로가 같은 트랜잭션에 참여하는지부터 확인해야 한다.
코드 예제
class PricePolicyTest {
@Test
void vipMembersReceiveDiscount() {
PricePolicy policy = new PricePolicy();
Money result = policy.discount(MemberGrade.VIP, Money.wons(10_000));
assertThat(result).isEqualTo(Money.wons(9_000));
}
}이 테스트에는 Spring이 필요 없다. 빠르고 명확하다.
@SpringBootTest
class OrderFlowIntegrationTest {
@Autowired OrderService orderService;
@Test
void placeOrder() {
Long orderId = orderService.placeOrder(new OrderCommand(...));
assertThat(orderId).isNotNull();
}
}이 테스트는 실제 Bean wiring과 트랜잭션 흐름을 확인한다. 이런 테스트는 값이 크지만 비용도 크므로 핵심 경로에 집중한다.
실전 팁
- 테스트를 작성하기 전 Spring 인프라가 정말 필요한지 먼저 묻는다.
@SpringBootTest는 핵심 통합 흐름에만 사용한다.- test profile과 test property 조합을 표준화해 context cache를 살린다.
@DirtiesContext는 정말 context 오염이 있을 때만 쓴다.- 같은 설정을 쓰는 테스트는 공통 meta-annotation이나 base class로 묶어 cache key가 불필요하게 갈라지지 않게 한다.
- 실패 원인이 너무 넓으면 단위 테스트나 slice 테스트로 분해한다.
위험 신호!
- 모든 테스트 클래스에
@SpringBootTest가 붙어 있다. - 테스트마다 다른 profile과 property 조합을 만든다.
@DirtiesContext가 습관적으로 붙어 있다.- 단위 테스트로 충분한 도메인 계산에 Spring Context를 띄운다.
- 통합 테스트 실패 원인을 찾기 위해 디버깅 시간이 과도하게 든다.
확인 질문
확인 질문
- TestContext의 context caching이 깨지는 대표 원인은 무엇인가?
- 테스트마다 profile, property, mock bean, context customizer 조합이 달라지는 것이다.
- 단위 테스트와 통합 테스트의 역할 차이는 무엇인가?
- 단위 테스트는 작은 로직을 빠르게 검증하고, 통합 테스트는 여러 Bean과 인프라가 함께 맞물리는지 검증한다.
@DirtiesContext를 남용하면 어떤 문제가 생기는가?
- context cache 재사용이 줄어 테스트가 크게 느려질 수 있다.