MockMvc 간단한 가이드 - ZZinBros/miniprojects-2019 GitHub Wiki

사용 용도

  1. Controller 계층에서 결과, 제어 흐름을 테스트할 때 쓴다.
  2. 실제 JPA가 어떻게 동작하는지 테스트하려면 @DataJPaTest로 RepositoryTest를 하거나, Service 테스트에서 @Autowired로 직접 테스트하는 게 좋다.
  3. 아래 방법이 절대 정답은 아니다. 다양한 방법이 존재하니 직접 찾아보는 것을 권장한다.

초기 세팅

클래스 Annotation은 다음과 같이 준다.

@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@TestInstance(value = TestInstance.Lifecycle.PER_CLASS)
class FriendControllerTest {

컨트롤러 클래스 안의 서비스 계층 위에는 @MockBean을, Controller에는 @Autowired를 쓴다.

    MockMvc mockMvc;

    @MockBean
    UserService userService;

    @MockBean
    FriendService friendService;

    @Autowired
    FriendController friendController;

MockMvc 객체를 만들 Builder는 다음과 같이 설정한다. 주로 @BeforeEach 혹은 @BeforeAll 등에서 설정한다.

    @BeforeAll
    void setUp() {
        mockMvc = MockMvcBuilders
            .standaloneSetup(friendController) // 위에서 선언한 컨트롤러
            .setControllerAdvice(new UserControllerExceptionAdvice()) // 예외 발생을 붙잡을 Advice
            .setCustomArgumentResolvers(new UserArgumentResolver()) // ArgumentResolver나 Advice도 가능
            .alwaysDo(print()) // 테스트 중에 각종 정보를 콘솔에 출력
            .build();
    }

주로 쓰는 함수

  1. given()
given(userService.register(userRequestDto)).willReturn(user);

Mocking을 통해 예상되는 입력에 대응하는 출력값을 테스트 코드에서 미리 정의해줄 수 있다. 이를 통해 제어 흐름을 선택적으로 테스트할 수 있다. 예를 들면 위 given()은 컨트롤러 내에서 userService.register()userRequestDto와 같은 입력을 받으면 user를 결과값으로 내어준다.

때로는 userService.register() 메소드가 어떤 값을 받아도 항상 user 객체를 반환하게끔 하고 싶을 때가 있을 것이다. 특히 테스트를 단순하게 만들기 위해서 이럴 필요가 있는데, 이럴 때는 인자 넣는 곳에 그냥 any()라고 넣어주면 된다.

given(userService.register(any())).willReturn(user);

any()는 다음과 같이 static import하면 쓸 수 있다.

import static org.mockito.ArgumentMatchers.any;

비슷한 것으로 anyBoolean()이나 anyInt(), anyString(), anyList()과 같은 여러 가지가 Mockito에 준비되어 있으므로, 그냥 편하게 가져다 쓰면 된다.

  1. verify()
verify(userService, times(1)).delete(BASE_ID, loginUserDto);

컨트롤러가 함수를 제대로 호출하는지 확인할 수 있다. 예를 들어 위의 예시 코드는 userService.delete()가 컨트롤러 내에서 1번 호출되는지 확인하는 함수. 첫번째 매개변수에 확인하고 싶은 객체, 두번째에 times()로 횟수를 지정한다.

  1. MockMvc.perform()으로 서버에 Form 입력값을 전송하는 경우를 테스트하기
mockMvc.perform(put("/users/" + BASE_ID) // HTTP Method, URL 지정
    .contentType(MediaType.APPLICATION_FORM_URLENCODED) // Form 입력일 때
    .sessionAttr(UserSession.LOGIN_USER, loginUserDto) // session 값을 집어넣을 수 있음
    .param("name", userUpdateDto.getName()) // form 입력값을 삽입하기 위해 param()을 써야 함 
    .param("email", userUpdateDto.getEmail()))
    .andExpect(status().is3xxRedirection()); // 상태코드 확인
  1. MockMvc.perform()으로 서버에 JSON을 전달하는 경우를 테스트하기
final String dtoString = new ObjectMapper()
    .writeValueAsString(friendRequestDto);

mockMvc.perform(post("/friends")
    .contentType(MediaType.APPLICATION_JSON_UTF8)
    .content(dtoString) // @RequestBody로 전달할 때
    .sessionAttr(UserSession.LOGIN_USER, loginUserDto))
    .andExpect(status().isFound());
  1. MockMvc.perform()으로 서버에서 JSON을 받아오는 경우를 테스트하기
mockMvc.perform(get(MAPPING_BY_POST_PATH + MOCK_ID))
    .andExpect(status().isOk())
    .andExpect(jsonPath("$.[0].contents", is("comment1")))
    .andExpect(jsonPath("$.[1].contents", is("comment2")))
    .andExpect(jsonPath("$.[2].contents", is("comment3")));

위와 같이 jsonPath()를 사용하면 된다.

주의할 점

  1. MockMvc 테스트를 할 때 설정해주는 입력·출력 객체는 반드시 equals()를 제대로 정의해야 한다.
  2. MockMvc 빌더에서 standaloneSetup()을 사용하는데 로그인이 필요할 때는 반드시 이를 처리해 줄 Resolver를 설정해줘야 한다.

Thymeleaf로 렌더링되는 컨트롤러를 Mock 테스트하려면

예를 들어서 다음과 같은 컨트롤러를 MockMvc로 테스트하려고 생각해 보자. 여기서 "signup"은 Thymeleaf 템플릿이다.

@Controller
public class DemoController {
    public DemoController() {}

    @GetMapping("/signup")
    public String signup() {
        return "signup";
    }
}

지금까지 나온 문서의 내용을 토대로 위 컨트롤러에 대해 MockMvc 테스트 코드를 작성하면 아마도 Circular view path…라는 ServletException이 발생하여 테스트가 멈출 것이다. 이 문제를 해결하려면 2가지 방법이 있다.

  1. URL Path와 Thymeleaf 템플릿의 이름이 서로 겹치지 않게 바꿔준다.
  2. MockMvc 테스트시 Thymeleaf 템플릿을 해석하게끔 한다.

여기서는 2번 방법에 대해 서술하겠다. 핵심은 MockMvcBuilders로 MockMvc 객체를 만들 때 빌더에서 standaloneSetup() 대신에 webAppContextSetup() 메소드를 사용하고, 이 메소드에는 @AutowiredWebApplicationContext 객체를 넣어주는 것이다. 해당 객체를 사용하려면 다음과 같이 import한다.

import org.springframework.web.context.WebApplicationContext;

이 방법을 사용하는 예제 테스트 코드는 다음과 같다. import 등은 생략하였다.

@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@TestInstance(value = TestInstance.Lifecycle.PER_CLASS)
class DemoControllerTest {
    private MockMvc mockMvc;

    @Autowired
    WebApplicationContext context;

    @Autowired
    DemoController demoController;

    @BeforeAll
    void setUp() {
        mockMvc = MockMvcBuilders
            .webAppContextSetup(context)
            .alwaysDo(print())
            .build();
    }

    @Test
    void signup() throws Exception {
        mockMvc
            .perform(get("/signup")
            .contentType(MediaType.APPLICATION_JSON_UTF8))
            .andExpect(status().isOk());
    }
}

이 방식은 로그인이 필요한 테스트가 있을 때도 별도로 Resolver를 설정해줄 필요가 없다.

참고

⚠️ **GitHub.com Fallback** ⚠️