🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Spring Test 완벽 가이드 - 슬라이드 1/11
A

AI Generated

2025. 10. 30. · 18 Views

Spring Test 완벽 가이드

Spring 애플리케이션의 테스트 전략부터 실전 기법까지 완벽하게 마스터하세요. @SpringBootTest, MockMvc, 단위 테스트, 통합 테스트 등 실무에서 바로 활용 가능한 테스트 작성법을 배웁니다.


목차

  1. @SpringBootTest
  2. @WebMvcTest
  3. @DataJpaTest
  4. MockMvc
  5. @MockBean
  6. TestContainers
  7. @Transactional
  8. AssertJ

1. @SpringBootTest

시작하며

여러분이 Spring Boot 애플리케이션을 개발하면서 "이 기능이 정말 제대로 동작할까?"라는 불안감을 느낀 적 있나요? 특히 여러 컴포넌트들이 함께 동작하는 복잡한 비즈니스 로직을 구현할 때, 단순히 코드를 작성하는 것만으로는 확신을 가질 수 없습니다.

실무에서는 Service 레이어가 Repository와 제대로 통신하는지, 트랜잭션이 정상적으로 관리되는지, 외부 API 호출이 예상대로 처리되는지 등 전체적인 흐름을 검증해야 합니다. 한 부분만 테스트해서는 실제 운영 환경에서 발생할 수 있는 문제를 발견하기 어렵습니다.

바로 이럴 때 필요한 것이 @SpringBootTest입니다. 이 어노테이션은 전체 Spring 애플리케이션 컨텍스트를 로드하여 실제 운영 환경과 가장 유사한 상태에서 테스트를 수행할 수 있게 해줍니다.

개요

간단히 말해서, @SpringBootTest는 Spring Boot 애플리케이션의 전체 컨텍스트를 테스트 환경에서 실행시켜주는 통합 테스트 어노테이션입니다. 실무에서는 여러 레이어(Controller, Service, Repository)가 함께 동작하면서 비즈니스 로직을 처리합니다.

예를 들어, 사용자 등록 기능은 컨트롤러에서 요청을 받아 서비스에서 검증을 하고, 레포지토리를 통해 데이터베이스에 저장하는 전체 플로우가 제대로 동작해야 합니다. 이런 경우에 @SpringBootTest가 매우 유용합니다.

기존에는 각 레이어를 개별적으로 테스트했다면, 이제는 전체 플로우를 하나의 테스트로 검증할 수 있습니다. 이 어노테이션의 핵심 특징은 첫째, 모든 Bean을 자동으로 로드한다는 점입니다.

둘째, 실제 애플리케이션 설정을 그대로 사용합니다. 셋째, 다양한 테스트 모드(MOCK, RANDOM_PORT, DEFINED_PORT)를 지원합니다.

이러한 특징들이 실제 운영 환경을 최대한 재현하여 신뢰성 높은 테스트를 가능하게 합니다.

코드 예제

@SpringBootTest
@AutoConfigureMockMvc
class UserServiceIntegrationTest {

    @Autowired
    private UserService userService;

    @Autowired
    private UserRepository userRepository;

    @Test
    @DisplayName("사용자 등록 통합 테스트")
    void shouldCreateUserSuccessfully() {
        // Given: 테스트할 사용자 데이터 준비
        UserCreateRequest request = new UserCreateRequest("john@example.com", "password123");

        // When: 실제 서비스 메서드 호출
        UserResponse response = userService.createUser(request);

        // Then: 데이터베이스에 실제로 저장되었는지 검증
        assertThat(response.getId()).isNotNull();
        assertThat(userRepository.findById(response.getId())).isPresent();
    }
}

설명

이것이 하는 일: @SpringBootTest는 테스트 실행 시 Spring Boot 애플리케이션을 실제로 구동시켜, 모든 Bean과 설정을 로드하고 전체 애플리케이션의 동작을 검증합니다. 첫 번째로, @SpringBootTest 어노테이션을 클래스에 선언하면 Spring TestContext Framework가 활성화됩니다.

이때 @SpringBootApplication이 선언된 메인 클래스를 찾아 그곳에 정의된 모든 설정과 Bean 스캔 규칙을 적용합니다. 왜 이렇게 하는지는 실제 운영 환경과 동일한 조건을 만들기 위함입니다.

그 다음으로, @Autowired를 통해 실제 Bean들을 주입받을 수 있습니다. 위 코드에서 UserService와 UserRepository는 실제 Spring 컨테이너에서 관리되는 Bean이며, 테스트 메서드 내에서 이들의 상호작용을 검증할 수 있습니다.

내부에서는 실제 데이터베이스 연결, 트랜잭션 관리, AOP 적용 등이 모두 실제와 동일하게 일어납니다. 테스트 메서드가 실행되면 Given-When-Then 패턴으로 시나리오를 구성합니다.

Given에서 테스트 데이터를 준비하고, When에서 실제 비즈니스 로직을 실행하며, Then에서 기대하는 결과가 나왔는지 검증합니다. 마지막으로, AssertJ의 assertThat을 사용하여 응답 객체와 데이터베이스의 상태를 모두 확인함으로써 전체 플로우가 정상 동작했음을 보장합니다.

여러분이 이 코드를 사용하면 컨트롤러부터 데이터베이스까지의 전체 플로우를 한 번의 테스트로 검증할 수 있습니다. 실무에서의 이점은 첫째, 레이어 간 통합 오류를 조기에 발견할 수 있고, 둘째, 실제 운영 환경에서 발생할 수 있는 설정 문제를 미리 감지하며, 셋째, 리팩토링 시 전체 기능이 여전히 동작함을 보장할 수 있다는 점입니다.

실전 팁

💡 @SpringBootTest는 전체 컨텍스트를 로드하므로 느립니다. 빠른 피드백이 필요한 단위 테스트에는 @WebMvcTest나 @DataJpaTest 같은 슬라이스 테스트를 사용하세요.

💡 webEnvironment 속성으로 테스트 모드를 조절할 수 있습니다. MOCK(기본값)은 모의 서블릿 환경, RANDOM_PORT는 실제 서버를 랜덤 포트로 실행합니다.

💡 @TestConfiguration을 사용하면 테스트 전용 Bean 설정을 추가할 수 있습니다. 예를 들어 외부 API를 모킹하거나 테스트 데이터를 초기화하는 Bean을 정의할 수 있습니다.

💡 @ActiveProfiles("test")를 함께 사용하여 테스트 전용 프로파일을 활성화하세요. application-test.yml에 테스트 DB 설정을 분리하면 운영 데이터를 보호할 수 있습니다.

💡 통합 테스트는 최소한으로 유지하고, 핵심 비즈니스 플로우에만 작성하세요. 너무 많은 통합 테스트는 빌드 시간을 급격히 증가시킵니다.


2. @WebMvcTest

시작하며

여러분이 REST API를 개발할 때 "이 엔드포인트가 올바른 HTTP 상태 코드를 반환하나?", "요청 파라미터 검증이 제대로 되나?"라는 의문이 들지 않나요? 특히 컨트롤러 레이어는 사용자와 직접 만나는 진입점이라 작은 실수도 치명적일 수 있습니다.

실무에서는 잘못된 요청 처리, 예외 핸들링 누락, 응답 형식 오류 등이 빈번하게 발생합니다. 전체 애플리케이션을 띄워서 테스트하면 시간이 오래 걸리고, Postman으로 수동 테스트하면 자동화가 어렵습니다.

바로 이럴 때 필요한 것이 @WebMvcTest입니다. 이 어노테이션은 컨트롤러 레이어만 집중적으로 테스트할 수 있게 해주며, 빠른 피드백 루프를 만들어줍니다.

개요

간단히 말해서, @WebMvcTest는 Spring MVC 컨트롤러만 로드하여 웹 레이어를 격리된 환경에서 빠르게 테스트할 수 있는 슬라이스 테스트 어노테이션입니다. 실무에서는 컨트롤러의 역할이 매우 중요합니다.

HTTP 요청을 받아 검증하고, 적절한 서비스를 호출하며, 응답을 올바른 형식으로 변환해야 합니다. 예를 들어, 상품 조회 API가 존재하지 않는 ID를 받았을 때 404를 반환하는지, 잘못된 형식의 요청에 400을 반환하는지 등을 검증하는 경우에 @WebMvcTest가 매우 유용합니다.

기존에는 전체 애플리케이션을 띄우고 실제 HTTP 요청을 보내야 했다면, 이제는 MockMvc를 통해 컨트롤러만 빠르게 테스트할 수 있습니다. 이 어노테이션의 핵심 특징은 첫째, @Controller, @RestController, @ControllerAdvice 등 웹 레이어 컴포넌트만 로드합니다.

둘째, Service나 Repository는 자동으로 모킹됩니다. 셋째, MockMvc를 자동으로 설정해줍니다.

이러한 특징들이 테스트 속도를 크게 향상시키면서도 컨트롤러의 동작을 정확하게 검증할 수 있게 합니다.

코드 예제

@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Test
    @DisplayName("사용자 조회 API 성공 테스트")
    void shouldReturnUserWhenValidId() throws Exception {
        // Given: 서비스 모킹 - 특정 ID로 조회하면 사용자 반환
        Long userId = 1L;
        UserResponse mockUser = new UserResponse(userId, "john@example.com");
        when(userService.getUserById(userId)).thenReturn(mockUser);

        // When & Then: GET 요청을 보내고 응답 검증
        mockMvc.perform(get("/api/users/{id}", userId))
            .andExpect(status().isOk())  // 200 상태 코드
            .andExpect(jsonPath("$.id").value(userId))  // JSON 응답의 id 필드
            .andExpect(jsonPath("$.email").value("john@example.com"));  // email 필드
    }
}

설명

이것이 하는 일: @WebMvcTest는 Spring MVC 인프라만 초기화하고, 지정한 컨트롤러와 관련 컴포넌트만 로드하여 HTTP 요청/응답 처리를 검증합니다. 첫 번째로, @WebMvcTest(UserController.class)를 선언하면 UserController와 관련된 웹 레이어 컴포넌트만 Spring 컨텍스트에 등록됩니다.

전체 애플리케이션을 로드하지 않기 때문에 테스트 시작 시간이 @SpringBootTest에 비해 월등히 빠릅니다. 왜 이렇게 하는지는 컨트롤러의 책임(HTTP 처리, 검증, 응답 변환)만 집중적으로 테스트하기 위함입니다.

그 다음으로, @MockBean으로 UserService를 모킹합니다. 실제 서비스 로직은 테스트하지 않고, 컨트롤러가 서비스를 올바르게 호출하는지만 확인합니다.

when().thenReturn() 구문으로 서비스의 예상 동작을 정의하면, 컨트롤러는 이 모의 객체와 상호작용하게 됩니다. 내부에서는 Mockito 프레임워크가 동작하여 실제 Bean 대신 모의 객체를 주입합니다.

테스트 메서드에서는 MockMvc의 perform() 메서드로 HTTP 요청을 시뮬레이션합니다. get("/api/users/{id}", userId)는 실제 HTTP GET 요청을 보내는 것처럼 동작하지만, 실제 네트워크 통신 없이 Spring MVC의 내부 메커니즘만 사용합니다.

마지막으로, andExpect() 체이닝으로 상태 코드, JSON 응답 필드 등을 검증하여 컨트롤러가 올바른 형식의 응답을 생성했는지 확인합니다. 여러분이 이 코드를 사용하면 컨트롤러의 매핑, 파라미터 바인딩, 검증 로직, 응답 변환 등을 빠르게 검증할 수 있습니다.

실무에서의 이점은 첫째, 1~2초 내에 테스트가 완료되어 TDD 방식으로 개발할 수 있고, 둘째, 서비스 레이어의 영향을 받지 않아 컨트롤러의 단독 책임을 명확히 테스트하며, 셋째, 다양한 HTTP 시나리오(성공, 실패, 예외)를 쉽게 시뮬레이션할 수 있다는 점입니다.

실전 팁

💡 @WebMvcTest는 기본적으로 Spring Security 설정도 로드합니다. 인증 없이 테스트하려면 @AutoConfigureMockMvc(addFilters = false)를 추가하세요.

💡 여러 컨트롤러를 함께 테스트하려면 @WebMvcTest({UserController.class, ProductController.class})처럼 배열로 지정할 수 있습니다.

💡 jsonPath() 대신 ObjectMapper로 JSON을 직접 파싱할 수도 있지만, jsonPath()가 더 간결하고 읽기 쉬운 검증 코드를 만들어줍니다.

💡 @ParameterizedTest와 함께 사용하면 다양한 입력값에 대한 검증을 한 번에 수행할 수 있습니다. 예를 들어 여러 잘못된 이메일 형식을 테스트할 때 유용합니다.

💡 MockMvcResultHandlers.print()를 andDo()에 연결하면 요청/응답의 전체 내용을 콘솔에 출력하여 디버깅할 수 있습니다.


3. @DataJpaTest

시작하며

여러분이 JPA 레포지토리를 작성하면서 "이 쿼리 메서드가 정말 원하는 데이터를 가져오나?", "N+1 문제가 발생하지 않을까?"라는 걱정을 해본 적 있나요? 특히 복잡한 @Query나 Specification을 사용할 때는 실제로 실행해보지 않으면 확신할 수 없습니다.

실무에서는 쿼리 메서드의 잘못된 네이밍, JPQL 문법 오류, 연관관계 설정 실수 등이 런타임에 발견되어 큰 문제를 일으킵니다. 전체 애플리케이션을 띄워서 테스트하면 너무 느리고, 실제 DB를 사용하면 테스트 데이터 관리가 복잡해집니다.

바로 이럴 때 필요한 것이 @DataJpaTest입니다. 이 어노테이션은 JPA 레포지토리와 관련된 컴포넌트만 로드하고, 인메모리 데이터베이스를 자동으로 설정하여 빠르고 안전한 레포지토리 테스트를 가능하게 합니다.

개요

간단히 말해서, @DataJpaTest는 JPA 관련 컴포넌트만 로드하고 인메모리 DB를 자동 설정하여 레포지토리 계층을 격리된 환경에서 테스트할 수 있는 슬라이스 테스트 어노테이션입니다. 실무에서는 데이터 접근 계층의 정확성이 매우 중요합니다.

잘못된 쿼리는 성능 저하나 데이터 무결성 문제를 일으킵니다. 예를 들어, 특정 조건으로 사용자를 검색하는 findByEmailAndActiveTrue() 같은 메서드가 실제로 올바른 SQL을 생성하는지, 페이징이 제대로 동작하는지 등을 검증하는 경우에 @DataJpaTest가 매우 유용합니다.

기존에는 실제 데이터베이스에 연결하거나 복잡한 설정이 필요했다면, 이제는 H2 인메모리 DB가 자동으로 설정되어 테스트가 빠르고 독립적으로 실행됩니다. 이 어노테이션의 핵심 특징은 첫째, @Repository와 JPA 관련 설정만 로드합니다.

둘째, 기본적으로 각 테스트 후 롤백되어 테스트 간 격리를 보장합니다. 셋째, TestEntityManager를 제공하여 테스트 데이터를 쉽게 준비할 수 있습니다.

이러한 특징들이 안전하고 빠른 데이터 계층 테스트를 가능하게 합니다.

코드 예제

@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    @DisplayName("이메일로 활성 사용자 조회 테스트")
    void shouldFindActiveUserByEmail() {
        // Given: 테스트 데이터를 영속성 컨텍스트에 저장
        User activeUser = User.builder()
            .email("active@example.com")
            .active(true)
            .build();
        entityManager.persist(activeUser);
        entityManager.flush();  // 즉시 DB에 반영

        // When: 레포지토리 메서드 호출
        Optional<User> found = userRepository.findByEmailAndActiveTrue("active@example.com");

        // Then: 올바른 사용자가 조회되었는지 검증
        assertThat(found).isPresent();
        assertThat(found.get().getEmail()).isEqualTo("active@example.com");
    }
}

설명

이것이 하는 일: @DataJpaTest는 JPA 관련 설정과 레포지토리만 로드하고, H2 같은 인메모리 데이터베이스를 자동 설정하여 데이터 접근 계층의 동작을 검증합니다. 첫 번째로, @DataJpaTest를 선언하면 Spring Data JPA 레포지토리, EntityManager, 데이터소스 설정만 활성화됩니다.

서비스나 컨트롤러 같은 다른 레이어는 로드되지 않아 테스트가 빠르게 시작됩니다. 또한 기본적으로 H2 인메모리 데이터베이스가 설정되므로 별도의 DB 설치나 설정 없이 바로 테스트할 수 있습니다.

왜 이렇게 하는지는 실제 DB의 영향을 받지 않고 순수하게 쿼리 로직만 테스트하기 위함입니다. 그 다음으로, TestEntityManager를 사용하여 테스트 데이터를 준비합니다.

일반 EntityManager와 달리 TestEntityManager는 테스트에 최적화된 메서드를 제공하며, persist()와 flush()를 통해 즉시 데이터베이스에 반영할 수 있습니다. 이렇게 준비된 데이터는 실제 레포지토리 메서드가 쿼리할 대상이 됩니다.

내부에서는 Hibernate가 엔티티를 SQL로 변환하고 H2 데이터베이스에 저장합니다. 테스트 메서드가 실행되면 userRepository.findByEmailAndActiveTrue()가 호출되고, Spring Data JPA가 메서드 이름을 분석하여 자동으로 쿼리를 생성합니다.

마지막으로, AssertJ의 assertThat()으로 결과를 검증하며, 테스트가 끝나면 @Transactional에 의해 자동으로 롤백되어 다음 테스트에 영향을 주지 않습니다. 여러분이 이 코드를 사용하면 복잡한 쿼리 메서드, JPQL, QueryDSL 등이 예상대로 동작하는지 빠르게 확인할 수 있습니다.

실무에서의 이점은 첫째, 실제 DB 없이도 쿼리를 검증할 수 있어 CI/CD 파이프라인에서도 안정적으로 실행되고, 둘째, N+1 문제나 불필요한 조인을 로그로 확인하여 성능 최적화할 수 있으며, 셋째, 테스트 간 데이터 격리가 자동으로 보장되어 순서에 관계없이 실행할 수 있다는 점입니다.

실전 팁

💡 @AutoConfigureTestDatabase(replace = NONE)을 추가하면 인메모리 DB 대신 실제 DB(예: MySQL)로 테스트할 수 있습니다. 단, 로컬 DB가 실행 중이어야 합니다.

💡 쿼리 실행 로그를 보려면 application-test.yml에 spring.jpa.show-sql=true와 logging.level.org.hibernate.SQL=DEBUG를 설정하세요.

💡 @Sql 어노테이션으로 테스트 전에 SQL 스크립트를 실행할 수 있습니다. 복잡한 테스트 데이터는 data.sql 파일로 관리하면 깔끔합니다.

💡 연관관계를 테스트할 때는 fetch 전략(LAZY/EAGER)에 주의하세요. 실제 사용 패턴과 동일하게 데이터를 조회해야 프로덕션 환경의 문제를 발견할 수 있습니다.

💡 페이징 테스트 시에는 충분한 양의 데이터를 준비하고, content뿐만 아니라 totalElements, totalPages 등도 함께 검증하세요.


4. MockMvc

시작하며

여러분이 REST API를 개발하면서 "이 엔드포인트가 정확한 JSON을 반환하나?", "예외 상황에서 적절한 에러 메시지를 주나?"라는 궁금증이 생기지 않나요? Postman으로 일일이 테스트하는 것은 시간도 오래 걸리고, 자동화도 어렵습니다.

실무에서는 API 스펙이 자주 변경되고, 다양한 HTTP 상태 코드와 헤더를 정확히 처리해야 합니다. 수동 테스트로는 모든 케이스를 커버하기 어렵고, 회귀 버그가 발생하기 쉽습니다.

바로 이럴 때 필요한 것이 MockMvc입니다. 이것은 실제 서버를 띄우지 않고도 Spring MVC의 전체 요청/응답 사이클을 시뮬레이션하여 자동화된 API 테스트를 가능하게 합니다.

개요

간단히 말해서, MockMvc는 실제 HTTP 서버 없이 Spring MVC 애플리케이션의 컨트롤러를 테스트할 수 있게 해주는 테스트 유틸리티입니다. 실무에서는 API의 다양한 측면을 검증해야 합니다.

HTTP 메서드, URL 패턴, 요청 파라미터, 헤더, 쿠키, 그리고 응답의 상태 코드, 본문, 헤더 등을 모두 확인해야 합니다. 예를 들어, 인증이 필요한 엔드포인트에 토큰 없이 접근했을 때 401을 반환하는지, POST 요청의 JSON 본문이 올바르게 파싱되는지 등을 검증하는 경우에 MockMvc가 매우 유용합니다.

기존에는 실제 서버를 실행하고 RestTemplate이나 WebClient로 HTTP 요청을 보내야 했다면, 이제는 MockMvc로 빠르고 가볍게 동일한 테스트를 수행할 수 있습니다. MockMvc의 핵심 특징은 첫째, 실제 네트워크 통신 없이 Spring MVC의 DispatcherServlet을 직접 호출합니다.

둘째, Fluent API로 요청 구성과 응답 검증을 직관적으로 작성할 수 있습니다. 셋째, JSON, XML 등 다양한 컨텐츠 타입을 지원합니다.

이러한 특징들이 빠르고 안정적인 API 테스트를 가능하게 합니다.

코드 예제

@WebMvcTest(ProductController.class)
class ProductControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private ProductService productService;

    @Test
    @DisplayName("상품 생성 API 테스트 - JSON 요청/응답")
    void shouldCreateProductSuccessfully() throws Exception {
        // Given: 요청 본문과 예상 응답 준비
        String requestBody = "{\"name\":\"Laptop\",\"price\":1200000}";
        ProductResponse response = new ProductResponse(1L, "Laptop", 1200000);
        when(productService.createProduct(any())).thenReturn(response);

        // When & Then: POST 요청 후 응답 검증
        mockMvc.perform(post("/api/products")
                .contentType(MediaType.APPLICATION_JSON)  // 요청 Content-Type
                .content(requestBody))  // 요청 본문
            .andExpect(status().isCreated())  // 201 상태
            .andExpect(header().exists("Location"))  // Location 헤더 존재
            .andExpect(jsonPath("$.id").value(1))  // 응답 JSON의 id
            .andExpect(jsonPath("$.name").value("Laptop"));  // name 필드
    }
}

설명

이것이 하는 일: MockMvc는 Spring MVC의 DispatcherServlet을 프로그래밍 방식으로 호출하여 HTTP 요청을 처리하고, 그 결과를 검증할 수 있는 API를 제공합니다. 첫 번째로, mockMvc.perform()으로 HTTP 요청을 시작합니다.

post("/api/products")는 POST 메서드로 해당 경로에 요청을 보내는 것을 의미하며, 실제로는 DispatcherServlet이 이 요청을 받아 적절한 컨트롤러 메서드로 라우팅합니다. contentType()과 content()로 요청의 헤더와 본문을 설정하는데, 이는 실제 HTTP 클라이언트가 보내는 것과 동일한 정보입니다.

왜 이렇게 하는지는 컨트롤러가 실제 환경에서 받을 요청을 그대로 재현하기 위함입니다. 그 다음으로, 설정된 요청이 Spring MVC의 전체 처리 파이프라인을 거칩니다.

ArgumentResolver가 JSON을 객체로 변환하고, 컨트롤러 메서드가 실행되며, HttpMessageConverter가 응답 객체를 JSON으로 직렬화합니다. 내부에서는 실제 운영 환경과 동일한 메커니즘이 작동하므로, 데이터 바인딩 오류나 직렬화 문제를 정확히 발견할 수 있습니다.

andExpect() 체이닝으로 응답을 다각도로 검증합니다. status().isCreated()는 HTTP 201 상태 코드를 확인하고, header().exists()는 특정 헤더의 존재를 검증하며, jsonPath()는 응답 JSON의 특정 필드 값을 확인합니다.

마지막으로, 이 모든 검증이 통과하면 컨트롤러가 HTTP 명세를 올바르게 준수하고 있음을 보장합니다. 여러분이 이 코드를 사용하면 API의 모든 측면을 자동화된 테스트로 검증할 수 있습니다.

실무에서의 이점은 첫째, 몇 밀리초 안에 테스트가 완료되어 빠른 피드백을 받을 수 있고, 둘째, 다양한 HTTP 시나리오(성공, 검증 실패, 인증 오류 등)를 코드로 문서화할 수 있으며, 셋째, CI/CD 파이프라인에서 자동으로 실행되어 API 스펙 변경을 즉시 감지할 수 있다는 점입니다.

실전 팁

💡 andDo(print())를 추가하면 전체 요청/응답을 콘솔에 출력하여 디버깅할 수 있습니다. 실패한 테스트를 분석할 때 매우 유용합니다.

💡 ObjectMapper로 객체를 JSON 문자열로 변환하면 요청 본문을 더 쉽게 작성할 수 있습니다: content(objectMapper.writeValueAsString(request))

💡 @AutoConfigureMockMvc를 @SpringBootTest와 함께 사용하면 전체 컨텍스트를 로드하면서도 MockMvc를 사용할 수 있습니다.

💡 MockMvcResultMatchers의 다양한 메서드를 활용하세요: content().string(), xpath(), cookie() 등으로 세밀한 검증이 가능합니다.

💡 인증이 필요한 엔드포인트는 .with(user("username").roles("ADMIN"))처럼 Spring Security 테스트 지원을 활용하여 인증 컨텍스트를 설정할 수 있습니다.


5. @MockBean

시작하며

여러분이 서비스 레이어를 테스트하면서 "외부 API 호출이나 데이터베이스 접근을 어떻게 처리하지?"라는 고민을 한 적 있나요? 특히 결제 게이트웨이, 이메일 발송, 외부 데이터 조회 같은 의존성이 있으면 테스트가 매우 복잡해집니다.

실무에서는 외부 시스템의 불안정성, 네트워크 지연, 비용 문제 등으로 인해 실제 의존성을 사용한 테스트가 어렵습니다. 또한 특정 예외 상황(타임아웃, 서버 오류 등)을 재현하기도 거의 불가능합니다.

바로 이럴 때 필요한 것이 @MockBean입니다. 이 어노테이션은 Spring 컨텍스트의 실제 Bean을 모의 객체로 대체하여, 외부 의존성을 완벽히 제어할 수 있게 해줍니다.

개요

간단히 말해서, @MockBean은 Spring ApplicationContext에 있는 실제 Bean을 Mockito 모의 객체로 교체하여 의존성을 격리하고 테스트를 단순화하는 Spring Boot 테스트 어노테이션입니다. 실무에서는 단위 테스트와 통합 테스트의 균형이 중요합니다.

서비스 레이어를 테스트할 때 모든 의존성을 실제로 구동하면 느리고 불안정하지만, 의존성의 동작을 제어할 수 있으면 빠르고 예측 가능한 테스트를 작성할 수 있습니다. 예를 들어, 주문 서비스가 결제 서비스를 호출할 때 결제 실패 상황을 시뮬레이션하여 재시도 로직이 제대로 동작하는지 검증하는 경우에 @MockBean이 매우 유용합니다.

기존에는 프로파일별로 다른 구현체를 만들거나 복잡한 테스트 설정이 필요했다면, 이제는 @MockBean으로 간단히 모의 객체를 주입할 수 있습니다. 이 어노테이션의 핵심 특징은 첫째, Spring 컨텍스트의 실제 Bean을 모의 객체로 교체합니다.

둘째, Mockito의 모든 기능(when, verify 등)을 사용할 수 있습니다. 셋째, 테스트 클래스 내에서만 유효하여 다른 테스트에 영향을 주지 않습니다.

이러한 특징들이 의존성을 격리하면서도 Spring의 DI 컨테이너를 활용한 테스트를 가능하게 합니다.

코드 예제

@SpringBootTest
class OrderServiceTest {

    @Autowired
    private OrderService orderService;

    @MockBean  // 실제 PaymentService를 모의 객체로 교체
    private PaymentService paymentService;

    @MockBean
    private EmailService emailService;

    @Test
    @DisplayName("결제 실패 시 주문 취소 및 이메일 발송")
    void shouldCancelOrderWhenPaymentFails() {
        // Given: 결제 서비스가 예외를 던지도록 모킹
        OrderRequest request = new OrderRequest("product-123", 2);
        when(paymentService.processPayment(any()))
            .thenThrow(new PaymentFailedException("카드 한도 초과"));

        // When: 주문 생성 시도
        assertThrows(OrderCreationException.class,
            () -> orderService.createOrder(request));

        // Then: 실패 이메일이 발송되었는지 검증
        verify(emailService).sendOrderFailureEmail(any());
    }
}

설명

이것이 하는 일: @MockBean은 Spring ApplicationContext에서 지정한 타입의 Bean을 찾아 Mockito 모의 객체로 교체하고, 테스트에서 이 모의 객체의 동작을 프로그래밍할 수 있게 합니다. 첫 번째로, @MockBean이 선언된 필드를 Spring이 감지하면 해당 타입의 실제 Bean 대신 Mockito가 생성한 모의 객체를 컨텍스트에 등록합니다.

OrderService는 @Autowired로 주입받지만, 그 안의 PaymentService와 EmailService는 실제 구현체가 아닌 모의 객체입니다. 왜 이렇게 하는지는 외부 시스템(결제, 이메일)에 의존하지 않고 OrderService의 로직만 테스트하기 위함입니다.

그 다음으로, when().thenThrow() 구문으로 모의 객체의 동작을 정의합니다. processPayment()가 호출되면 PaymentFailedException을 던지도록 설정하여, 실제로 결제 API를 호출하지 않고도 실패 상황을 시뮬레이션합니다.

내부에서는 Mockito가 메서드 호출을 가로채고, 사전에 정의한 동작을 실행합니다. 테스트 메서드가 실행되면 orderService.createOrder()는 실제 비즈니스 로직을 수행하지만, 의존성들은 모두 제어된 모의 객체입니다.

마지막으로, verify()로 특정 메서드가 호출되었는지 검증하여, OrderService가 실패 시 이메일을 발송하는 로직을 올바르게 구현했는지 확인합니다. 여러분이 이 코드를 사용하면 외부 시스템 없이도 복잡한 비즈니스 로직을 테스트할 수 있습니다.

실무에서의 이점은 첫째, 외부 API의 다양한 응답(성공, 실패, 타임아웃)을 쉽게 시뮬레이션할 수 있고, 둘째, 네트워크 비용이나 외부 서비스 의존성 없이 빠르게 실행되며, 셋째, 엣지 케이스(예: 동시성 문제, 부분 실패)를 코드로 재현하여 견고성을 검증할 수 있다는 점입니다.

실전 팁

💡 @MockBean 대신 @SpyBean을 사용하면 실제 Bean을 부분적으로 모킹할 수 있습니다. 일부 메서드만 모킹하고 나머지는 실제 동작을 유지할 때 유용합니다.

💡 when()에 특정 인자를 지정하려면 eq()를 사용하세요: when(service.method(eq("specific"))).thenReturn(result)

💡 여러 번 호출될 때 다른 응답을 주려면 thenReturn을 체이닝하세요: when(service.method()).thenReturn(first).thenReturn(second)

💡 verify()에 times(), never(), atLeastOnce() 등을 추가하여 호출 횟수를 정확히 검증할 수 있습니다.

💡 @MockBean은 Spring 컨텍스트를 재생성하므로 남용하면 테스트가 느려집니다. 가능하면 같은 모의 Bean 구성을 공유하는 테스트들을 그룹화하세요.


6. TestContainers

시작하며

여러분이 인메모리 H2 데이터베이스로 테스트하면서 "실제 PostgreSQL에서도 이 쿼리가 동작할까?"라는 의문을 가져본 적 있나요? 특히 데이터베이스별 고유 기능(JSON 타입, 전문 검색, 특정 함수)을 사용하면 H2와 실제 DB 간의 차이가 큰 문제가 됩니다.

실무에서는 프로덕션과 동일한 데이터베이스를 사용하지 않으면 배포 후 예상치 못한 SQL 오류가 발생합니다. 로컬에 실제 DB를 설치하는 것은 환경 설정이 복잡하고, 팀원마다 다른 버전을 사용하면 일관성이 깨집니다.

바로 이럴 때 필요한 것이 TestContainers입니다. 이 라이브러리는 Docker 컨테이너를 활용하여 실제 데이터베이스를 테스트마다 자동으로 시작하고 종료하여, 프로덕션과 동일한 환경에서 안전하게 테스트할 수 있게 해줍니다.

개요

간단히 말해서, TestContainers는 Docker 컨테이너 기반으로 실제 데이터베이스, 메시지 큐, 캐시 등의 외부 시스템을 테스트 환경에 자동으로 프로비저닝하는 Java 라이브러리입니다. 실무에서는 데이터베이스의 특정 기능에 의존하는 경우가 많습니다.

PostgreSQL의 JSONB 타입, MySQL의 전문 검색, MongoDB의 집계 파이프라인 등은 인메모리 DB로 재현할 수 없습니다. 예를 들어, JSON 필드를 쿼리하는 네이티브 SQL을 사용하거나, 특정 버전의 DB에만 있는 기능을 활용하는 경우에 TestContainers가 매우 유용합니다.

기존에는 로컬에 DB를 수동으로 설치하고 관리해야 했다면, 이제는 테스트 코드만으로 필요한 인프라가 자동으로 준비됩니다. TestContainers의 핵심 특징은 첫째, 테스트 시작 시 Docker 컨테이너를 자동으로 실행하고 종료합니다.

둘째, 프로덕션과 동일한 DB 버전과 설정을 사용할 수 있습니다. 셋째, PostgreSQL, MySQL, Redis, Kafka 등 다양한 인프라를 지원합니다.

이러한 특징들이 개발 환경과 프로덕션 환경의 괴리를 최소화하여 더 신뢰성 높은 테스트를 가능하게 합니다.

코드 예제

@SpringBootTest
@Testcontainers  // TestContainers 활성화
class UserRepositoryIntegrationTest {

    @Container  // PostgreSQL 컨테이너 정의
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");

    @DynamicPropertySource  // Spring에 컨테이너 연결 정보 제공
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private UserRepository userRepository;

    @Test
    @DisplayName("PostgreSQL JSON 필드 쿼리 테스트")
    void shouldQueryJsonField() {
        // Given: JSON 데이터를 포함한 사용자 저장
        User user = new User("john@example.com", "{\"preferences\": {\"theme\": \"dark\"}}");
        userRepository.save(user);

        // When: JSON 경로로 쿼리 (PostgreSQL 고유 기능)
        List<User> users = userRepository.findByJsonPreference("theme", "dark");

        // Then: 올바르게 조회되었는지 검증
        assertThat(users).hasSize(1);
        assertThat(users.get(0).getEmail()).isEqualTo("john@example.com");
    }
}

설명

이것이 하는 일: TestContainers는 JUnit 테스트가 시작될 때 지정한 Docker 이미지로 컨테이너를 실행하고, 테스트가 끝나면 자동으로 정리하여 실제 인프라를 사용한 격리된 테스트 환경을 제공합니다. 첫 번째로, @Testcontainers와 @Container 어노테이션이 선언되면 JUnit이 테스트 클래스 실행 전에 PostgreSQL 컨테이너를 시작합니다.

postgres:15-alpine 이미지가 Docker Hub에서 다운로드되고(캐시되어 있으면 재사용), 지정한 설정으로 데이터베이스가 초기화됩니다. static 필드로 선언하여 모든 테스트 메서드가 같은 컨테이너를 공유하므로 속도가 빠릅니다.

왜 이렇게 하는지는 실제 PostgreSQL의 모든 기능을 테스트에서 사용하기 위함입니다. 그 다음으로, @DynamicPropertySource로 컨테이너의 연결 정보를 Spring의 프로퍼티로 등록합니다.

postgres.getJdbcUrl()은 컨테이너가 실행된 후의 실제 접속 URL(예: jdbc:postgresql://localhost:49153/testdb)을 반환하며, Spring Boot가 이 정보로 DataSource를 설정합니다. 내부에서는 Hibernate가 실제 PostgreSQL에 연결하여 스키마를 생성하고 JPA 엔티티를 테이블로 매핑합니다.

테스트 메서드가 실행되면 findByJsonPreference() 같은 PostgreSQL 특화 쿼리가 실제 데이터베이스에서 실행됩니다. H2에서는 지원하지 않는 JSONB 타입, JSON 경로 연산자 등이 모두 정상 동작합니다.

마지막으로, 테스트가 끝나면 TestContainers가 컨테이너를 자동으로 중지하고 삭제하여 깨끗한 상태를 유지합니다. 여러분이 이 코드를 사용하면 프로덕션 데이터베이스의 모든 기능을 안전하게 테스트할 수 있습니다.

실무에서의 이점은 첫째, DB 특화 기능을 사용해도 테스트가 실패하지 않아 기술 선택의 자유도가 높아지고, 둘째, 팀 전체가 동일한 DB 버전으로 테스트하여 "내 환경에서는 되는데" 문제가 사라지며, 셋째, CI/CD 파이프라인에서도 Docker만 있으면 실행되어 별도의 DB 서버 관리가 필요 없다는 점입니다.

실전 팁

💡 컨테이너 시작은 시간이 걸리므로 static 필드로 선언하여 클래스당 한 번만 시작하세요. 매 테스트마다 재시작하면 매우 느려집니다.

💡 .withReuse(true)를 설정하고 ~/.testcontainers.properties에 testcontainers.reuse.enable=true를 추가하면 여러 테스트 실행 간 컨테이너를 재사용할 수 있습니다.

💡 Docker Desktop이나 Docker Daemon이 실행 중이어야 합니다. CI 환경에서는 Docker-in-Docker나 DinD 설정이 필요할 수 있습니다.

💡 GenericContainer를 사용하면 공식 모듈이 없는 이미지도 사용할 수 있습니다: new GenericContainer<>("custom-image:latest")

💡 로그 확인이 필요하면 .withLogConsumer(new Slf4jLogConsumer(logger))를 추가하여 컨테이너 로그를 테스트 로그로 출력할 수 있습니다.


7. @Transactional

시작하며

여러분이 테스트를 작성하면서 "이전 테스트에서 생성한 데이터가 다음 테스트에 영향을 주면 어쩌지?"라는 걱정을 해본 적 있나요? 특히 여러 테스트가 같은 데이터베이스를 공유하면 테스트 순서에 따라 결과가 달라지는 불안정한 상황이 발생합니다.

실무에서는 테스트 간 데이터 격리가 매우 중요합니다. 한 테스트가 다른 테스트의 데이터를 변경하거나 삭제하면 디버깅이 극도로 어려워지고, 테스트의 신뢰성이 떨어집니다.

또한 테스트 후 수동으로 데이터를 정리하는 것은 번거롭고 실수하기 쉽습니다. 바로 이럴 때 필요한 것이 @Transactional입니다.

이 어노테이션을 테스트 클래스나 메서드에 적용하면 각 테스트가 독립된 트랜잭션에서 실행되고, 테스트 종료 후 자동으로 롤백되어 데이터베이스를 깨끗한 상태로 유지합니다.

개요

간단히 말해서, @Transactional은 테스트 메서드를 트랜잭션으로 감싸고 테스트 종료 시 자동으로 롤백하여 테스트 간 데이터 격리를 보장하는 Spring 어노테이션입니다. 실무에서는 통합 테스트나 레포지토리 테스트에서 실제 데이터베이스에 쓰기 작업을 해야 하는 경우가 많습니다.

사용자 등록, 주문 생성, 상품 수정 등의 기능을 테스트할 때 실제로 데이터를 저장해야 전체 플로우를 검증할 수 있습니다. 예를 들어, 회원가입 기능을 테스트할 때 실제로 DB에 사용자를 저장하고 조회해봐야 하지만, 이 데이터가 다른 테스트에 영향을 주면 안 됩니다.

이런 경우에 @Transactional이 매우 유용합니다. 기존에는 @Before에서 데이터를 삽입하고 @After에서 삭제하는 복잡한 설정이 필요했다면, 이제는 어노테이션 하나로 자동 롤백이 처리됩니다.

@Transactional의 핵심 특징은 첫째, 테스트 메서드 전체를 하나의 트랜잭션으로 관리합니다. 둘째, 테스트 성공 여부와 무관하게 항상 롤백됩니다(테스트 컨텍스트의 기본 동작).

셋째, @Commit을 추가하면 롤백 대신 커밋할 수도 있습니다. 이러한 특징들이 테스트 데이터 관리의 복잡성을 제거하고 일관된 테스트 환경을 보장합니다.

코드 예제

@SpringBootTest
@Transactional  // 클래스 레벨에 선언하면 모든 테스트 메서드에 적용
class OrderServiceIntegrationTest {

    @Autowired
    private OrderService orderService;

    @Autowired
    private OrderRepository orderRepository;

    @Test
    @DisplayName("주문 생성 후 자동 롤백 테스트")
    void shouldCreateOrderAndRollback() {
        // Given: 주문 요청 데이터
        OrderRequest request = new OrderRequest("product-123", 5);

        // When: 주문 생성 (실제로 DB에 INSERT)
        OrderResponse response = orderService.createOrder(request);

        // Then: 트랜잭션 내에서는 데이터 조회 가능
        assertThat(response.getId()).isNotNull();
        Optional<Order> found = orderRepository.findById(response.getId());
        assertThat(found).isPresent();

        // 테스트 종료 후 자동으로 ROLLBACK되어 실제 DB에는 데이터가 남지 않음
    }

    @Test
    @DisplayName("이전 테스트의 영향을 받지 않음")
    void shouldStartWithCleanDatabase() {
        // 이전 테스트가 롤백되었으므로 DB는 깨끗한 상태
        assertThat(orderRepository.findAll()).isEmpty();
    }
}

설명

이것이 하는 일: @Transactional은 테스트 메서드 실행 전에 트랜잭션을 시작하고, 메서드 내의 모든 데이터베이스 작업을 하나의 트랜잭션으로 묶은 후, 테스트가 끝나면 자동으로 롤백하여 데이터베이스를 원래 상태로 되돌립니다. 첫 번째로, 테스트 메서드가 시작되기 전에 Spring TestContext Framework가 트랜잭션을 시작합니다.

이후 메서드 내의 모든 데이터베이스 작업(INSERT, UPDATE, DELETE)은 이 트랜잭션 안에서 실행됩니다. orderService.createOrder()가 실제로 데이터를 저장하지만, 아직 커밋되지 않은 상태이므로 다른 트랜잭션에서는 보이지 않습니다.

왜 이렇게 하는지는 테스트 내에서는 실제 동작을 검증하면서도 외부에는 영향을 주지 않기 위함입니다. 그 다음으로, 테스트 메서드 내에서는 같은 트랜잭션에 속해 있으므로 방금 생성한 데이터를 조회할 수 있습니다.

orderRepository.findById()가 정상적으로 데이터를 반환하며, assertThat으로 검증이 가능합니다. 내부에서는 Hibernate의 1차 캐시와 트랜잭션 격리 수준에 따라 데이터가 관리됩니다.

테스트 메서드가 종료되면(성공이든 실패든) Spring은 자동으로 ROLLBACK을 실행합니다. 마지막으로, 롤백된 데이터베이스는 테스트 시작 전과 동일한 상태가 되어, 다음 테스트가 깨끗한 환경에서 실행됩니다.

두 번째 테스트 메서드에서 findAll()이 빈 리스트를 반환하는 것이 이를 증명합니다. 여러분이 이 코드를 사용하면 테스트 순서나 다른 테스트의 영향을 전혀 받지 않는 독립적인 테스트를 작성할 수 있습니다.

실무에서의 이점은 첫째, 테스트 실패 시 데이터 정리를 신경 쓰지 않아도 되고, 둘째, 병렬 실행 시에도 격리가 보장되어 속도 향상이 가능하며, 셋째, 복잡한 데이터 설정 코드(@Before/@After) 없이 간결한 테스트를 유지할 수 있다는 점입니다.

실전 팁

💡 @DataJpaTest는 기본적으로 @Transactional을 포함하므로 별도로 선언할 필요가 없습니다. 중복 선언해도 무해하지만 불필요합니다.

💡 실제로 커밋이 필요한 테스트(예: 트랜잭션 이벤트 리스너 검증)는 @Commit 또는 @Rollback(false)을 추가하세요.

💡 여러 트랜잭션 간의 상호작용을 테스트하려면 @Transactional을 제거하고 수동으로 트랜잭션을 관리해야 합니다.

💡 읽기 전용 테스트에는 @Transactional(readOnly = true)를 사용하면 성능 최적화와 의도 명확화에 도움이 됩니다.

💡 일부 JPA 작업(예: flush, clear)은 트랜잭션 컨텍스트에서만 동작하므로, @Transactional 없이는 테스트하기 어려운 경우가 있습니다.


8. AssertJ

시작하며

여러분이 테스트 코드를 작성하면서 "assertEquals(expected, actual)을 쓸 때마다 순서가 헷갈려"라는 경험을 한 적 있나요? 또한 복잡한 객체나 컬렉션을 검증할 때 JUnit의 기본 assert 문법이 너무 장황하고 읽기 어렵다고 느끼지 않나요?

실무에서는 테스트 코드의 가독성이 매우 중요합니다. 테스트가 실패했을 때 어떤 조건이 깨졌는지 빠르게 파악해야 하고, 다른 개발자가 테스트 의도를 즉시 이해할 수 있어야 합니다.

전통적인 assert 문법은 에러 메시지가 불친절하고, 복잡한 검증을 위해서는 많은 코드가 필요합니다. 바로 이럴 때 필요한 것이 AssertJ입니다.

이 라이브러리는 유창한(fluent) API로 자연스러운 영어 문장처럼 검증 코드를 작성할 수 있게 하며, 실패 시 상세한 에러 메시지를 자동으로 생성합니다.

개요

간단히 말해서, AssertJ는 메서드 체이닝 방식의 유창한 API를 제공하여 가독성 높고 표현력 풍부한 검증 코드를 작성할 수 있게 해주는 Java 테스트 라이브러리입니다. 실무에서는 다양한 타입의 데이터를 검증해야 합니다.

객체의 특정 필드값, 컬렉션의 크기와 내용, 예외의 메시지, Optional의 존재 여부 등 수많은 시나리오가 있습니다. 예를 들어, API 응답 객체가 여러 필드를 가지고 있을 때 각 필드를 개별적으로 검증하는 대신 한 번에 여러 조건을 명확하게 표현하는 경우에 AssertJ가 매우 유용합니다.

기존에는 assertTrue(), assertEquals() 등을 반복해서 사용했다면, 이제는 assertThat()으로 시작하는 체이닝으로 모든 검증을 직관적으로 표현할 수 있습니다. AssertJ의 핵심 특징은 첫째, assertThat(actual).isEqualTo(expected) 형태로 실제값을 먼저 쓰는 자연스러운 순서입니다.

둘째, IDE의 자동완성으로 사용 가능한 검증 메서드를 쉽게 발견할 수 있습니다. 셋째, 실패 시 "expected: <...> but was: <...>" 같은 명확한 에러 메시지를 자동 생성합니다.

이러한 특징들이 테스트 코드의 가독성과 유지보수성을 크게 향상시킵니다.

코드 예제

@Test
@DisplayName("사용자 응답 객체 복합 검증 - AssertJ 활용")
void shouldValidateUserResponseWithAssertJ() {
    // Given: 사용자 데이터 준비
    User user = userService.createUser(new UserRequest("alice@example.com", "Alice", 25));

    // When: 응답 객체 생성
    UserResponse response = UserResponse.from(user);

    // Then: AssertJ로 여러 조건 검증
    assertThat(response)
        .isNotNull()  // null이 아님
        .extracting("email", "name", "age")  // 필드 추출
        .containsExactly("alice@example.com", "Alice", 25);  // 순서대로 일치

    // 컬렉션 검증
    List<String> roles = response.getRoles();
    assertThat(roles)
        .hasSize(2)  // 크기 검증
        .contains("USER", "MEMBER")  // 포함 여부
        .doesNotContain("ADMIN");  // 미포함 확인

    // Optional 검증
    Optional<String> nickname = response.getNickname();
    assertThat(nickname)
        .isPresent()  // 값이 존재
        .hasValue("alice_25");  // 특정 값 확인
}

설명

이것이 하는 일: AssertJ는 assertThat()으로 시작하는 유창한 API를 통해 다양한 타입의 데이터를 직관적으로 검증하고, 검증 실패 시 컨텍스트를 포함한 상세한 에러 메시지를 자동 생성합니다. 첫 번째로, assertThat(response)를 호출하면 AssertJ가 response 객체의 타입을 분석하고 적절한 assertion 객체를 반환합니다.

이 객체는 해당 타입에 특화된 수십 개의 검증 메서드를 제공하며, IDE의 자동완성이 이를 제안합니다. isNotNull(), extracting() 같은 메서드들이 체이닝되어 마치 영어 문장처럼 읽힙니다.

왜 이렇게 하는지는 코드를 읽는 사람이 테스트 의도를 즉시 파악할 수 있게 하기 위함입니다. 그 다음으로, extracting()으로 객체의 여러 필드를 한 번에 추출하고, containsExactly()로 예상값과 비교합니다.

전통적인 방법이라면 세 개의 assertEquals가 필요했을 것이지만, AssertJ는 이를 한 줄로 표현합니다. 내부에서는 리플렉션을 사용하여 필드값을 추출하고, 각 값을 순서대로 비교합니다.

컬렉션 검증에서는 hasSize(), contains(), doesNotContain() 같은 전용 메서드를 사용합니다. 각 메서드는 실패 시 "expected size: <2> but was: <3>" 또는 "expected to contain: <ADMIN> but did not" 같은 구체적인 메시지를 출력합니다.

마지막으로, Optional 검증도 isPresent(), hasValue()로 간결하게 처리하여, 복잡한 조건문 없이 의도를 명확히 드러냅니다. 여러분이 이 코드를 사용하면 테스트의 의도가 즉시 파악되고, 실패 시 원인을 빠르게 찾을 수 있습니다.

실무에서의 이점은 첫째, 신입 개발자도 테스트 코드를 쉽게 이해하고 작성할 수 있어 온보딩 시간이 단축되고, 둘째, 리팩토링 시 변경된 부분을 테스트 코드에서 빠르게 찾아 수정할 수 있으며, 셋째, 코드 리뷰 시 테스트의 검증 범위를 한눈에 파악하여 누락된 케이스를 발견하기 쉽다는 점입니다.

실전 팁

💡 as() 메서드로 커스텀 에러 메시지를 추가할 수 있습니다: assertThat(user.getAge()).as("사용자 나이 검증").isGreaterThan(18)

💡 SoftAssertions를 사용하면 여러 검증을 실행하고 마지막에 한 번에 실패 여부를 확인할 수 있어, 하나가 실패해도 나머지를 계속 검증합니다.

💡 usingRecursiveComparison()로 객체의 모든 필드를 재귀적으로 비교할 수 있습니다. DTO 비교 시 매우 유용합니다.

💡 Custom Assertion을 만들어 도메인 특화 검증을 재사용할 수 있습니다. 예: assertThat(order).isCompleted().hasItemCount(5)

💡 Exception 검증은 assertThatThrownBy()를 사용하세요: assertThatThrownBy(() -> service.method()).isInstanceOf(CustomException.class).hasMessage("오류 메시지")


#Spring#JUnit5#MockMvc#TestAnnotation#Integration#React

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스

이전4/4
다음