티스토리 뷰

반응형

안녕하세요!

오늘은 Spring MVC 컨트롤러를 테스트할 때 자주 사용되는 두 도구, MockMvcMockMvcTester를 비교해보겠습니다.

Spring Framework는 웹 애플리케이션 테스트를 위해 다양한 도구를 제공하는데, 이 중 MockMvc는 오랜 기간 표준으로 자리 잡았고, 최근 Spring 6.2부터 소개된 MockMvcTester는 더 현대적이고 fluent한 API를 제공합니다. 이 포스팅에서는 먼저 각 개념을 설명한 후, 비교를 통해 차이점을 분석하고, 마지막으로 실제 예시 코드를 통해 어떻게 사용하는지 보여드리겠습니다.

이 비교는 Spring Boot를 기반으로 한 테스트 환경을 가정하며, 실제 프로젝트에서 컨트롤러 테스트를 효율적으로 적용하는 데 도움이 될 것입니다. 그럼 시작해볼까요?

1. 개념 설명

1.1 MockMvc란?

MockMvc는 Spring Framework의 Test 모듈에서 제공하는 핵심 클래스입니다. 주로 MVC 컨트롤러를 테스트하기 위해 설계되었으며, 실제 서버를 구동하지 않고도 HTTP 요청을 시뮬레이션할 수 있게 해줍니다. 이는 단위 테스트나 통합 테스트에서 매우 유용합니다. MockMvc를 사용하면 DispatcherServlet을 통해 요청을 처리하고, 응답을 검증할 수 있습니다.

MockMvc의 주요 특징은 다음과 같습니다:

  • 모킹 기반 테스트: 실제 데이터베이스나 외부 서비스를 모킹(mock)하여 컨트롤러 레이어만 집중적으로 테스트할 수 있습니다.
  • 빌더 패턴 사용: MockMvcBuilders를 통해 설정하며, @WebMvcTest 어노테이션과 함께 사용하면 Spring Boot에서 자동으로 MockMvc 인스턴스를 주입받을 수 있습니다.
  • HTTP 메서드 지원: GET, POST, PUT, DELETE 등 다양한 HTTP 메서드를 지원하며, 헤더, 쿼리 파라미터, 바디 등을 자유롭게 설정할 수 있습니다.
  • 검증 기능: 응답 상태 코드, 헤더, 바디 내용을 검증할 수 있지만, 기본적으로 JUnit의 assertions를 사용합니다.

예를 들어, Spring Boot 프로젝트에서 @WebMvcTest를 사용하면 전체 애플리케이션 컨텍스트를 로드하지 않고 MVC 슬라이스만 로드하여 빠른 테스트가 가능합니다. 이는 테스트 속도를 높이고, 불필요한 빈(Bean) 로드를 피하는 데 효과적입니다. MockMvc는 Spring 3.x부터 존재해온 안정적인 도구로, 많은 레거시 프로젝트에서 여전히 사용되고 있습니다.

그러나 MockMvc의 API는 다소 verbose(장황)할 수 있어, 코드가 길어지기 쉽습니다. 예를 들어, 요청 빌드와 응답 검증이 여러 줄에 걸쳐 작성되곤 합니다.

1.2 MockMvcTester란?

MockMvcTester는 Spring Framework 6.2에서 새롭게 소개된 API로, 기존 MockMvc를 기반으로 하지만 더 fluent하고 표현력 있는 테스트를 작성할 수 있도록 설계되었습니다. 이는 AssertJ 라이브러리와 긴밀하게 통합되어 assertions를 더 자연스럽게 사용할 수 있게 합니다. MockMvcTester의 목적은 개발자들이 더 읽기 쉽고 유지보수하기 쉬운 테스트 코드를 작성하도록 돕는 것입니다.

MockMvcTester의 주요 특징은 다음과 같습니다:

  • Fluent API: 체인 메서드를 통해 요청과 검증을 한 줄로 연결할 수 있습니다. 이는 코드의 가독성을 크게 향상시킵니다.
  • AssertJ 통합: 기본 assertions 대신 AssertJ의 fluent assertions를 사용해, 더 세밀하고 표현력 있는 검증이 가능합니다. 예를 들어, JSON 응답을 쉽게 파싱하고 검증할 수 있습니다.
  • MockMvc 기반: 내부적으로 MockMvc를 사용하므로, 기존 MockMvc 테스트를 쉽게 마이그레이션할 수 있습니다. MockMvcTester.from(mockMvc)처럼 기존 MockMvc 인스턴스를 래핑합니다.
  • 추가 assertions: HTTP 응답의 JSON, XML, 또는 텍스트를 직접적으로 assert할 수 있는 메서드를 제공합니다. 예를 들어, body().jsonPath().hasJsonPathValue() 같은 메서드가 내장되어 있습니다.
  • Spring Boot 호환: Spring Boot 3.2 이상에서 잘 동작하며, @WebMvcTest와 함께 사용 가능합니다.

MockMvcTester는 Petri Kainulainen 같은 개발자 커뮤니티에서 "더 깨끗한 테스트 작성"을 강조하며 소개되었습니다. JetBrains의 가이드에서도 MockMvcTester를 사용하면 custom assertions를 통해 결과를 더 표현력 있게 검증할 수 있다고 언급합니다. 이는 특히 복잡한 API 응답을 다룰 때 유용합니다. 그러나 Spring 6.2 이전 버전에서는 사용할 수 없으므로, 프로젝트의 Spring 버전에 따라 선택해야 합니다.

요약하자면, MockMvc는 전통적이고 안정적인 기반 도구이며, MockMvcTester는 그 위에 현대적인 레이어를 추가한 업그레이드 버전이라고 볼 수 있습니다.

2. 비교 분석

이제 MockMvc와 MockMvcTester를 여러 측면에서 비교해보겠습니다. 비교를 위해 테이블 형식을 사용하겠습니다. 이는 각 도구의 강점과 약점을 명확히 보여줄 것입니다.

2.1 주요 차이점 테이블

항목 MockMvc MockMvcTester
소개 시기 Spring 3.x부터 (오랜 역사) Spring 6.2부터 (최근 도입)
API 스타일 빌더 패턴 기반, verbose (장황한 코드) Fluent API, 체인 메서드 (간결하고 읽기 쉬움)
Assertions JUnit이나 Hamcrest 같은 기본 assertions 사용 AssertJ 통합으로 더 표현력 있는 assertions (e.g., JSON 검증 쉬움)
가독성 코드가 여러 줄로 나뉘어 다소 복잡함 한 줄 체인으로 작성 가능, 유지보수 용이
호환성 모든 Spring 버전에서 사용 가능 Spring 6.2 이상 필요
성능 가볍고 빠름 (MVC 슬라이스 테스트) MockMvc 기반이므로 비슷하지만, 추가 레이어로 약간 오버헤드 있을 수 있음
커스텀 확장 직접 matcher나 helper 메서드 작성 필요 내장 custom assertions 제공 (e.g., body assertions)
마이그레이션 - 기존 MockMvc 테스트를 쉽게 변환 가능 (from(mockMvc) 메서드)

2.2 장단점 비교

  • MockMvc의 장점:
    • 안정성과 호환성: 오래된 도구라서 문서와 예제가 풍부합니다. Spring Boot 1.x부터 사용 가능하므로 레거시 프로젝트에 적합합니다.
    • 가벼움: 추가 의존성 없이 기본 Spring Test로 충분합니다.
    • 유연성: 저수준 API라서 세밀한 커스터마이징이 가능합니다. 예를 들어, custom filter나 interceptor를 쉽게 추가할 수 있습니다.
  • MockMvc의 단점:
    • 코드 길이: 요청 빌드(perform()), 응답 검증(andExpect())이 반복되어 코드가 길어집니다. 가독성이 떨어질 수 있습니다.
    • Assertions 한계: 기본 matcher가 제한적이라, 복잡한 JSON 검증 시 JsonPath나 추가 라이브러리가 필요합니다.
  • MockMvcTester의 장점:
    • 가독성과 표현력: Fluent API 덕분에 테스트 코드가 자연어처럼 읽힙니다. AssertJ 통합으로 "isEqualTo" 대신 "hasSize", "containsExactly" 같은 메서드를 사용해 더 직관적입니다.
    • 생산성 향상: custom assertions가 내장되어 있어, boilerplate 코드가 줄어듭니다. 예를 들어, 응답 바디를 직접 JSON으로 파싱하고 검증할 수 있습니다.
    • 현대적 접근: 최근 트렌드에 맞춰 테스트 코드를 더 declarative하게 작성할 수 있습니다.
  • MockMvcTester의 단점:
    • 버전 의존성: Spring 6.2 이상이 필요하므로, 오래된 프로젝트에서는 업그레이드가 필요합니다.
    • 학습 곡선: 기존 MockMvc 사용자라면 새로운 API를 익혀야 합니다. 하지만 마이그레이션이 쉽다는 점이 보완됩니다.
    • 오버헤드: AssertJ 의존성을 추가해야 할 수 있으며, 매우 간단한 테스트에서는 과도할 수 있습니다.

2.3 언제 어떤 걸 사용하나?

  • MockMvc 추천 시나리오: 간단한 테스트, 레거시 프로젝트, 또는 저수준 제어가 필요할 때. @SpringBootTest와 함께 전체 컨텍스트를 테스트할 때도 적합합니다.
  • MockMvcTester 추천 시나리오: 새로운 프로젝트, 복잡한 API 검증, 또는 코드 가독성을 우선할 때. 특히 팀에서 AssertJ를 이미 사용 중이라면 자연스럽게 채택할 수 있습니다.

전반적으로 MockMvcTester는 MockMvc의 진화된 형태로, 미래 지향적 선택입니다. 하지만 프로젝트 상황에 따라 선택하세요.

3. 예시 코드

이제 실제 코드를 통해 어떻게 사용하는지 보겠습니다. 간단한 REST 컨트롤러를 가정합니다. 컨트롤러는 /users 엔드포인트로 사용자 목록을 반환하는 GET 메서드를 가집니다.

3.1 테스트 대상 컨트롤러

먼저, 예시 컨트롤러 코드입니다:

@RestController
@RequestMapping("/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping
    public List<User> getAllUsers() {
        return userService.findAll();
    }
}

UserService는 모킹할 서비스입니다.

3.2 MockMvc를 사용한 예시 테스트

MockMvc를 사용한 테스트 코드는 다음과 같습니다. @WebMvcTest를 사용해 MVC 슬라이스만 로드합니다.

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

import java.util.Arrays;
import java.util.List;

import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(UserController.class)
class UserControllerMockMvcTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Test
    void getAllUsers_ShouldReturnUserList() throws Exception {
        // Given
        List<User> users = Arrays.asList(new User(1L, "John"), new User(2L, "Jane"));
        when(userService.findAll()).thenReturn(users);

        // When & Then
        mockMvc.perform(MockMvcRequestBuilders.get("/users")
                .accept("application/json"))
                .andExpect(status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(2))
                .andExpect(MockMvcResultMatchers.jsonPath("$[0].name").value("John"))
                .andExpect(MockMvcResultMatchers.jsonPath("$[1].name").value("Jane"));
    }
}

이 코드에서 보듯, perform()andExpect()를 반복 사용합니다. JsonPath를 통해 응답을 검증하지만, 코드가 여러 줄입니다.

3.3 MockMvcTester를 사용한 예시 테스트

MockMvcTester를 사용하면 같은 테스트를 더 fluent하게 작성할 수 있습니다. AssertJ를 의존성에 추가해야 합니다.

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MockMvcTester;

import java.util.Arrays;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;

@WebMvcTest(UserController.class)
class UserControllerMockMvcTesterTest {

    private final MockMvcTester tester;

    @MockBean
    private UserService userService;

    public UserControllerMockMvcTesterTest(@Autowired MockMvc mockMvc) {
        this.tester = MockMvcTester.from(mockMvc);
    }

    @Test
    void getAllUsers_ShouldReturnUserList() {
        // Given
        List<User> users = Arrays.asList(new User(1L, "John"), new User(2L, "Jane"));
        when(userService.findAll()).thenReturn(users);

        // When & Then
        tester.perform(get("/users").accept("application/json"))
              .expectStatus().isOk()
              .expectBody()
                .jsonPath("$.length()").isEqualTo(2)
                .jsonPath("$[0].name").isEqualTo("John")
                .jsonPath("$[1].name").isEqualTo("Jane")
              .andExpect(result -> assertThat(result.getResponse().getContentAsString()).contains("John"));
    }
}

여기서 tester.perform() 후 체인으로 expectStatus(), expectBody()를 연결합니다. AssertJ의 isEqualTo()가 더 자연스럽습니다. 추가로 custom assert를 쉽게 추가할 수 있습니다.

3.4 추가 예시: POST 요청 테스트

MockMvc로 POST 테스트:

@Test
void createUser_ShouldReturnCreatedUser() throws Exception {
    // Given
    User newUser = new User(3L, "Alice");
    when(userService.save(any(User.class))).thenReturn(newUser);

    // When & Then
    mockMvc.perform(MockMvcRequestBuilders.post("/users")
            .contentType("application/json")
            .content("{\"name\":\"Alice\"}"))
            .andExpect(status().isCreated())
            .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("Alice"));
}

MockMvcTester로 POST 테스트:

@Test
void createUser_ShouldReturnCreatedUser() {
    // Given
    User newUser = new User(3L, "Alice");
    when(userService.save(any(User.class))).thenReturn(newUser);

    // When & Then
    tester.perform(post("/users")
            .contentType("application/json")
            .content("{\"name\":\"Alice\"}"))
          .expectStatus().isCreated()
          .expectBody()
            .jsonPath("$.name").isEqualTo("Alice")
          .andExpect(result -> assertThat(result.getResponse().getStatus()).isEqualTo(201));
}

MockMvcTester 쪽이 더 간결합니다.

결론

MockMvc와 MockMvcTester는 둘 다 훌륭한 도구지만, MockMvcTester가 더 현대적이고 개발자 친화적입니다. 프로젝트에 따라 선택하세요. 만약 Spring 6.2로 업그레이드할 수 있다면 MockMvcTester를 추천합니다.

반응형