테스트 전략과 Spring TestContext
3줄 요약
Spring 테스트는 순수 단위 테스트, slice 테스트, 통합 테스트를 목적에 따라 조합해야 한다.
TestContext Framework는 Spring ApplicationContext 로딩, 의존성 주입, 트랜잭션 테스트, context caching을 제공한다.
테스트가 느려지는 가장 흔한 이유는 모든 검증을 @SpringBootTest로 처리해 컨텍스트를 과하게 많이 띄우기 때문이다.
핵심 정리
단위 테스트는 Spring 없이 객체를 직접 생성해 도메인 규칙과 서비스 분기를 빠르게 검증한다.
Slice 테스트는 MVC, JPA, JSON, REST client 같은 특정 계층만 작은 Spring Context로 검증한다.
통합 테스트는 여러 Bean과 실제 설정이 함께 맞물리는지를 검증한다.
TestContext는 테스트 ApplicationContext를 캐싱한다. active profile, test property, context customizer, mock bean 조합이 불필요하게 늘어나면 캐시 효율이 떨어져 테스트가 느려진다.
TestContext의 트랜잭션 테스트는 기본적으로 테스트 후 rollback되지만, 실제 서버 thread나 비동기 작업처럼 테스트 메서드와 다른 실행 흐름은 같은 rollback 경계에 있다고 가정하면 안 된다.
좋은 테스트 전략은 빠른 피드백과 실제 인프라 검증 사이의 균형을 잡는다.
헷갈리는 지점
Spring 프로젝트이므로 모든 테스트가 Spring Context를 띄워야 한다고 생각하기 쉽다. DI와 Bean이 핵심이라 그렇게 느껴진다.
핵심은 순수 비즈니스 규칙은 Spring 없이도 검증할 수 있다는 점이다.
Spring 인프라가 필요한 테스트만 Context를 사용한다.
통합 테스트가 많을수록 품질이 높다고 보기 쉽다. 실제 환경과 가까워 보이기 때문이다.
핵심은 느리고 원인 추적이 어려운 테스트만 늘어날 수 있다는 점이다.
중요한 wiring과 인프라 흐름은 통합 테스트로, 세부 분기는 단위 테스트로 나눈다.
테스트가 느려지면 장비를 늘리는 것부터 생각하기 쉽다. 실행 시간이 직접 문제로 보이기 때문이다.
핵심은 context caching이 깨지는 설정 조합을 줄이는 것이다.
profile, property, mock bean 조합을 무분별하게 늘리지 않는다.
확인 질문
Spring TestContext Framework가 제공하는 대표 기능은 무엇인가?
ApplicationContext 로딩과 캐싱, 테스트 의존성 주입, 트랜잭션 테스트 지원을 제공한다.
모든 테스트에 @SpringBootTest를 쓰면 어떤 문제가 생길 수 있는가?
컨텍스트 로딩이 느려지고 실패 원인 추적이 어려워질 수 있다.
단위 테스트가 Spring 없이도 가능한 이유는 무엇인가?
핵심 도메인 규칙과 서비스 분기는 일반 Java 객체 협력으로 검증할 수 있기 때문이다.
Unit Test와 Test Double
3줄 요약
단위 테스트는 Spring Container 없이 객체를 직접 만들고 협력 객체를 fake, stub, mock으로 대체해 빠르게 검증한다.
테스트 더블은 외부 의존성을 제어하기 위한 도구이지 구현 세부 호출을 과하게 고정하는 수단이 아니다.
좋은 단위 테스트는 결과와 상태 변화를 검증하고, 불필요한 interaction 검증을 줄인다.
핵심 정리
도메인 객체, 값 객체, 정책 객체, 계산 로직은 순수 단위 테스트가 가장 빠르고 안정적이다.
Service 테스트도 Repository나 외부 client를 fake로 대체하면 Spring 없이 핵심 분기를 검증할 수 있다.
Mock은 외부 호출 여부가 중요한 경우에 사용한다. 하지만 모든 메서드 호출 순서를 검증하면 리팩터링에 취약해진다.
Fake는 실제처럼 동작하는 단순 구현체다. 복잡한 mock 설정보다 읽기 쉬운 경우가 많다.
단위 테스트는 빠른 피드백을 제공하고, 통합 테스트는 wiring과 인프라 계약을 보완한다.
헷갈리는 지점
Mock을 많이 쓸수록 테스트가 세밀하다고 생각하기 쉽다. 호출 하나하나를 확인할 수 있기 때문이다.
핵심은 과한 interaction 검증이 구현을 고정한다는 점이다.
중요한 외부 부작용만 검증하고 결과 중심으로 테스트한다.
Repository가 필요하면 반드시 Spring 테스트가 필요하다고 생각하기 쉽다. Repository가 Bean으로 주입되기 때문이다.
핵심은 Service 분기를 보려는지 실제 DB 쿼리를 보려는지 나누는 것이다.
Service 분기는 fake Repository로, DB 쿼리는 JPA/JDBC slice 테스트로 검증한다.
단위 테스트는 Spring을 안 쓰니 가치가 낮다고 오해하기 쉽다. 실제 앱과 떨어져 보이기 때문이다.
핵심은 도메인 규칙의 대부분이 Spring 없이도 재현 가능해야 한다는 점이다.
순수 테스트가 어렵다면 비즈니스 로직이 프레임워크에 과하게 묶였다는 신호일 수 있다.
확인 질문
단위 테스트가 가장 잘 맞는 대상은 무엇인가?
도메인 규칙, 값 객체, 정책 객체, 계산 로직처럼 Spring 인프라 없이 검증 가능한 코드다.
Mock 검증을 과하게 하면 어떤 문제가 생기는가?
구현 세부 호출 순서에 테스트가 묶여 리팩터링에 취약해진다.
Fake가 Mock보다 나을 수 있는 경우는 언제인가?
협력 객체의 동작을 단순하고 읽기 쉬운 실제 구현처럼 표현할 수 있을 때다.
WebMvcTest와 MockMvc
3줄 요약
@WebMvcTest는 MVC 계층만 로드하는 slice 테스트이고, MockMvc는 실제 Servlet Container 없이 MVC 요청을 수행한다.
Controller 테스트는 request mapping, validation, binding, message conversion, exception advice를 함께 검증하는 데 강하다.
Service와 Repository까지 모두 검증하려면 WebMvcTest가 아니라 통합 테스트나 별도 계층 테스트를 사용해야 한다.
핵심 정리
MockMvc는 가상의 Servlet 환경에서 DispatcherServlet을 통해 요청을 실행한다.
@WebMvcTest는 Controller, MVC infrastructure, Jackson, Validation, ControllerAdvice 같은 웹 계층에 집중한다. 낮은 수준의 Servlet container 동작까지 검증하는 도구는 아니다.
Controller가 의존하는 Service는 테스트 더블로 대체한다. 관심은 Controller의 HTTP 계약이지 Service 내부 로직이 아니다.
JSON 응답은 jsonPath, status, content type, header 검증으로 계약을 고정한다.
Security가 적용된 Controller 테스트는 Spring Security 테스트 지원과 함께 인증 사용자, CSRF, 권한을 명시해야 한다.
헷갈리는 지점
MockMvc를 쓰면 서버 전체가 뜬다고 생각하기 쉽다. HTTP 요청처럼 작성하기 때문이다.
핵심은 MockMvc가 실제 서버를 띄우지 않고 Servlet API mock 환경에서 MVC 흐름을 실행한다는 점이다.
네트워크, 실제 포트, container 설정까지 검증하려면 다른 테스트가 필요하다.
WebMvcTest에서 Service 로직까지 검증하려 하기 쉽다. Controller가 Service를 호출하므로 한 번에 보고 싶기 때문이다.
핵심은 WebMvcTest의 목적이 웹 계층 계약 검증이라는 점이다.
Service 로직은 단위 테스트나 통합 테스트에서 따로 검증한다.
Security 테스트에서 인증을 생략하고 401/403을 Controller 실패로 오해하기 쉽다. Controller 테스트에 보안 필터가 개입하기 때문이다.
핵심은 보호 API 테스트에는 인증 principal과 CSRF 여부를 명시해야 한다는 점이다.
기대 상태가 200인지 401인지 403인지 의도를 먼저 정한다.
확인 질문
@WebMvcTest의 목적은 무엇인가?
MVC 웹 계층을 작은 context로 로드해 Controller HTTP 계약을 검증하는 것이다.
MockMvc는 실제 서버를 띄우는가?
아니다. mock Servlet 환경에서 DispatcherServlet 요청 흐름을 실행한다.
WebMvcTest에서 Service 의존성은 보통 어떻게 처리하는가?
테스트 더블로 대체해 Controller의 요청/응답 계약에 집중한다.
Integration Test와 Testcontainers
3줄 요약
통합 테스트는 여러 Spring Bean, 설정, 트랜잭션, 데이터 접근, 보안 흐름이 함께 맞물리는지 확인한다.
@SpringBootTest는 넓은 context를 로드하므로 꼭 필요한 경계에서 사용해야 한다.
DB, Redis, Kafka 같은 외부 인프라는 Testcontainers로 실제에 가까운 환경을 만들 수 있지만 비용과 안정성을 함께 관리해야 한다.
핵심 정리
통합 테스트는 자동 설정, Bean wiring, transaction, serialization, security filter, database query가 함께 동작하는지 검증한다.
@DataJpaTest는 JPA slice 테스트에 적합하고, @SpringBootTest는 애플리케이션 전체 흐름 검증에 적합하다.
Testcontainers는 테스트 중 실제 컨테이너 기반 인프라를 띄워 운영 DB와 더 비슷한 검증을 가능하게 한다. 최신 Spring Boot에서는 @ServiceConnection으로 컨테이너 연결 정보를 자동 설정에 연결할 수 있지만, slice 테스트에서는 embedded database replacement 설정도 함께 확인해야 한다.
통합 테스트가 많아질수록 context cache, container reuse, test data isolation, transaction rollback 전략이 중요해진다.
모든 테스트를 실제 인프라로 돌리는 것보다 위험이 큰 경로를 선별해 통합 테스트로 보호하는 것이 좋다.
헷갈리는 지점
H2로 테스트가 통과하면 운영 DB도 안전하다고 생각하기 쉽다. SQL이 대부분 비슷하게 동작하기 때문이다.
핵심은 DB dialect, lock, index, constraint, JSON type, timezone 동작이 다를 수 있다는 점이다.
DB 의존 쿼리는 운영 DB와 같은 종류의 컨테이너로 검증하는 편이 안전하다.
@SpringBootTest가 가장 실제에 가까우니 항상 최선이라고 보기 쉽다. 전체 애플리케이션을 로드하기 때문이다.
핵심은 느린 테스트는 자주 실행되지 않고 실패 원인도 넓어진다는 점이다.
slice와 단위 테스트로 대부분을 커버하고 핵심 흐름만 통합 테스트로 둔다.
테스트 데이터는 아무렇게나 넣어도 rollback되니 괜찮다고 생각하기 쉽다. 테스트가 끝나면 지워질 것처럼 느끼기 때문이다.
핵심은 비동기, 별도 트랜잭션, 컨테이너 재사용, sequence 값은 rollback만으로 완전히 격리되지 않을 수 있다는 점이다.
데이터 준비와 정리 전략을 명확히 해야 한다.
확인 질문
@SpringBootTest를 모든 테스트에 쓰면 안 되는 이유는 무엇인가?
context가 넓어 느리고 실패 원인 추적이 어려워질 수 있기 때문이다.
Testcontainers가 H2보다 유리한 경우는 언제인가?
운영 DB와 dialect, constraint, lock, 확장 기능 차이가 중요한 쿼리를 검증할 때다.
통합 테스트의 핵심 목적은 무엇인가?
여러 Spring Bean과 실제 설정, 외부 인프라 계약이 함께 맞물리는지 확인하는 것이다.