[테스트] MockMvc와 실제 ServletContainer 통합 테스트

MockMvc VS 실제 Servlet Container

MockMvc를 활용한 테스트

MockMvc란?

  • MockMvc는 서블릿 컨테이너의 구동 없이, 시뮬레이션된 MVC 환경에 모의 HTTP 서블릿 요청을 전송하는 기능을 제공하는 유틸리티 클래스다.
  • 즉, 주로 Test에 사용되는 (특히 유닛테스트) 가짜 HTTP 통신 기능을 제공하는 클래스이다.
  • 실제 환경에서의 컨테이너 환경(DispatcherServlet 등)과 다르기 때문에, 예상치 못한 문제가 발생할 수 있다.
    • MockMvc를 사용하면, Dispatcher Servlet이 MockUp 된다.
  • @WebMvcTest를 통해 오직 컨트롤러만 테스트할 때나, @SpringBootTest를 통해 통합테스트를 할 때, 모두 사용할 수 있다.

보다 상세한 내용은 “이 포스팅 글”을 참고하자.



MockMvc를 활용한 통합 테스트 - 예시 코드

UserRestController 클래스

  • 아래 코드는 사용자 회원가입을 처리하는 컨트롤러 클래스이다.
package com.four.brothers.runtou.controller;

import ...

@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/user")
public class UserRestController {
  private final UserService userService;

  @Operation(summary = "유저 회원가입")
  @PostMapping("/signup/orderer")
  public SignUpAsOrdererResponse signUpAsOrderer(
    @Parameter(name = "회원 정보")
    @Validated @RequestBody SignUpAsOrdererRequest request,
    BindingResult bindingResult, HttpServletRequest requestMsg
  ) {
    boolean result = false;

    if (bindingResult.hasFieldErrors()) {
      throw new BadRequestException(RequestExceptionCode.WRONG_FORMAT, bindingResult.getFieldError().getDefaultMessage());
    }

    try {
      result = userService.signUpAsOrderer(request);
    } catch (DataIntegrityViolationException e) {
      throw new BadRequestException(SignupExceptionCode.ALREADY_EXIST_INFO, "회원가입 정보가 중복됩니다.");
    }

    return new SignUpAsOrdererResponse(result);
  }

}


UserIntegrationTest 테스트 클래스

import com.fasterxml.jackson.databind.ObjectMapper;
import com.four.brothers.runtou.dto.OrdererDto;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@AutoConfigureMockMvc //MockMvc 사용 설정
@SpringBootTest(
  properties = {
    "spring.config.location=classpath:application-local.properties",
    "spring.config.location=classpath:application.properties"
  }
  /* 아래 속성은 Default이므로 생략할 수 있다.
   * webEnvironment = SpringBootTest.WebEnvironment.MOCK
   */
)
public class UserIntegrationTest {
  @Autowired
  private MockMvc mockMvc;
  @Autowired
  private ObjectMapper objectMapper;

  @Transactional
  @DisplayName("사용자 회원가입")
  @Test
  void userSignUpTest() throws Exception {
    //GIVEN
    String accountId1 = "testAccountId1";
    String realName1 = "testRealName1";
    String nickname1 = "testNickname1";
    String password1 = "testPassword1";
    String phoneNumber1 = "01011112222";
    String accountNumber1 = "1234567890";

    String content;

    OrdererDto.SignUpAsOrdererRequest request = new OrdererDto.SignUpAsOrdererRequest();
    request.setAccountId(accountId1);
    request.setRealName(realName1);
    request.setNickname(nickname1);
    request.setPassword(password1);
    request.setPhoneNumber(phoneNumber1);
    request.setAccountNumber(accountNumber1);

    content = objectMapper.writeValueAsString(request);

    //WHEN-THEN (MockUp된 Dispatcher Servlet에 요청을 보낸다.)
    mockMvc.perform(
      post("/api/user/signup/orderer")
        .content(content)
        .accept("*/*")
        .contentType(MediaType.APPLICATION_JSON)
    ).andExpect(status().isOk());
  }
}



실제 Servlet Container를 활용한 테스트

  • 이번에는 실제 Servlet Container를 활용하여 테스트해보자.


UserRestController 클래스

  • 위에서 작성한 코드와 완전히 동일하다.


UserIntegrationTest 테스트 클래스

import com.four.brothers.runtou.dto.OrdererDto;
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.transaction.annotation.Transactional;

@SpringBootTest(
  properties = {
    "spring.config.location=classpath:application-local.properties",
    "spring.config.location=classpath:application.properties"
  },
  //실제 서블릿 컨테이너를 사용하기 위해, 사용할 포트를 랜덤으로 설정
  webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
public class UserIntegrationTest {
  //TestRestTemplete 객체를 사용하여, 테스트를 진행한다.
  @Autowired
  private TestRestTemplate template;

  @Transactional
  @DisplayName("사용자 회원가입")
  @Test
  void userSignUpTest() {
    //GIVEN
    String accountId1 = "testAccountId1";
    String realName1 = "testRealName1";
    String nickname1 = "testNickname1";
    String password1 = "testPassword1";
    String phoneNumber1 = "01011112222";
    String accountNumber1 = "1234567890";

    OrdererDto.SignUpAsOrdererRequest request = new OrdererDto.SignUpAsOrdererRequest();
    request.setAccountId(accountId1);
    request.setRealName(realName1);
    request.setNickname(nickname1);
    request.setPassword(password1);
    request.setPhoneNumber(phoneNumber1);
    request.setAccountNumber(accountNumber1);

    //WHEN (실제 서블릿 컨테이너에 요청을 보낸다.)
    HttpStatus resultStatusCode = template.postForEntity("/api/user/signup/orderer", request, OrdererDto.SignUpAsOrdererResponse.class)
      .getStatusCode();

    //THEN
    assertEquals(true, resultStatusCode.is2xxSuccessful());
  }
}