테스트 전략과 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과 실제 설정, 외부 인프라 계약이 함께 맞물리는지 확인하는 것이다.