Spring : MockMvc

MockMVC를 이용한 WebAPI 테스트

원래는 Web API를 작성하고 테스트를 수행하기 위해서 스프링 애플리케이션을 실행하고 개발자가 직접 테스트를 수행했습니다. Web API를 많이 작성하다보면 웹 어플리케이션을 실행하고 브라우저를 열어서 테스트할 URI를 입력하고 다시 코드를 작성하고 웹 어플리케이션을 재시작하고 등을 반복하게 됩니다. Web API를 실행하는 시간보다 웹 어플리케이션을 실행하고 종료하는 시간이 더 오래걸리는 상황이 발생합니다.

여기에는 다음과 같은 문제점이 있습니다.

  • 개발자의 수동 테스트
  • 코드를 수정한 후에 서버를 재시작하고 테스트

이런 문제를 해결하기 위해 다음과 같은 방법을 사용할 수 있습니다.

MockMVC 란?

우리는 웹 애플리케이션을 작성한 후, 해당 웹 애플리케이션을 Tomcat이라는 이름의 WAS(Web Application Server)에 배포(deploy)하여 실행을 하였습니다. 브라우저의 요청은 WAS에게 전달되는 것이고 응답도 WAS에게서 받게 됩니다. WAS는 요청을 받은 후, 해당 요청을 처리하는 웹 어플리케이션을 실행하게 됩니다.

즉, Web API를 테스트한다는 것은 WAS를 실행해야만 된다는 문제가 있습니다. 이런 문제를 해결하기 위해서 스프링 3.2부터 MockMVC가 추가되었습니다.

MockMVCWAS와 같은 역할을 수행합니다. 요청을 받고 응답을 받는 WAS와 같은 역할을 수행하면서 여러분들이 작성한 웹 애플리케이션을 실행해줍니다.
WAS는 실행 시 상당한 많은 작업을 수행합니다. 하지만 MockMVC는 웹 어플리케이션을 실행하기 위한 최소한의 기능만을 가지고 있기 때문MockMVC를 이용한 웹 어플리케이션 실행은 상당히 빠릅니다.

MockMVC를 이용하면 다음과 같은 테스트를 수행할 수 있습니다.

img 출처 : https://www.slideshare.net/sbcoba/spring-test-mvc (26 page)

예제를 통해 알아보는 MockMVC Test

GuestbookApiController를 테스트하는 GuestbookApiControllerTest 클래스를 작성해보도록 하겠습니다.

먼저, 아래 그림을 보겠습니다.

img

GuestbookApiController를 단위 테스트한다는 것은, GuestbookApiController가 사용하는 GuestbookService에 대한 부분은 함께 테스트하지 않는다는 것을 의미합니다. 이를 위해 GuestbookService에 대한 목(Mock)객체를 사용할 것이고 Mokito를 이용해 목객체를 생성할 것입니다.

이에 대한 자세한 내용은 로직 단위테스트를 봐주세요~!

그리고, 컨트롤러를 테스트하기 위해 MockMvc를 사용하도록 하겠습니다.

테스트 전에 pom.xml에 라이브러리를 추가해줍니다.

<dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>1.9.5</version>
            <scope>test</scope>
</dependency>

GestubookApiControllerTest.java

package kr.or.connect.guestbook.controller;

import kr.or.connect.guestbook.config.ApplicationConfig;
import kr.or.connect.guestbook.config.WebMvcContextConfiguration;
import kr.or.connect.guestbook.dto.Guestbook;
import kr.or.connect.guestbook.service.GuestbookService;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

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

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

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes = {WebMvcContextConfiguration.class, ApplicationConfig.class })
public class GestubookApiControllerTest {
    @InjectMocks
    public GuestbookApiController guestbookApiController;

    @Mock
    GuestbookService guestbookService;

    private MockMvc mockMvc;

    @Before
    public void createController() {
        MockitoAnnotations.initMocks(this);
        mockMvc = MockMvcBuilders.standaloneSetup(guestbookApiController).build();
    }

    @Test
    public void getGuestbooks() throws Exception {
        Guestbook guestbook1 = new Guestbook();
        guestbook1.setId(1L);
        guestbook1.setRegdate(new Date());
        guestbook1.setContent("hello");
        guestbook1.setName("kim");

        List<Guestbook> list = Arrays.asList(guestbook1);
        when(guestbookService.getGuestbooks(0)).thenReturn(list);

        RequestBuilder reqBuilder = MockMvcRequestBuilders.get("/guestbooks").contentType(MediaType.APPLICATION_JSON);
        mockMvc.perform(reqBuilder).andExpect(status().isOk()).andDo(print());

        verify(guestbookService).getGuestbooks(0);
    }

    @Test
    public void deleteGuestbook() throws Exception {
        Long id = 1L;

        when(guestbookService.deleteGuestbook(id, "127.0.0.1")).thenReturn(1);

        RequestBuilder reqBuilder = MockMvcRequestBuilders.delete("/guestbooks/" + id).contentType(MediaType.APPLICATION_JSON);
        mockMvc.perform(reqBuilder).andExpect(status().isOk()).andDo(print());

        verify(guestbookService).deleteGuestbook(id, "127.0.0.1");
    }
}
@Mock
GuestbookService guestbookService;

@Mock어노테이션을 붙여서 선언된 guestbookServicemockito에 의해 목객체로 생성됩니다. 말그대로 가짜 객체가 됩니다.

@InjectMocks
public GuestbookApiController guestbookApiController;

@InjectMocks어노테이션이 붙여서 선언된 guestbookApiController는 목객체인 GuestbookService를 사용하게 됩니다. 스프링에 의해 주입된 객체를 사용하는 것이 아닌 Mockito 프레임워크에 의해 생성된 목객체가 주입되어 객체가 생성됩니다.

	@Before
    public void createController() {
          .......
   }
}

테스트 메소드가 실행되기 전에 @Before어노테이션이 붙은 메소드가 실행됩니다.

MockitoAnnotations.initMocks(this);

현재 객체에서 @Mock이 붙은 필드를 목객체로 초기화시킵니다.

mockMvc = MockMvcBuilders.standaloneSetup(guestbookApiController).build();

MockMVC타입의 변수 mockMvc를 초기화 합니다. guestbookApiController를 테스트 하기 위한 MockMvc객체를 생성합니다.

       Guestbook guestbook1 = new Guestbook();
        guestbook1.setId(1L);
        guestbook1.setRegdate(new Date());
        guestbook1.setContent("hello");
        guestbook1.setName("kim");

        List<Guestbook> list = Arrays.asList(guestbook1);
        when(guestbookService.getGuestbooks(0)).thenReturn(list);

List<Guestbook>타입의 변수 list를 초기화하고 해당 list에 방명록 한 건을 저장합니다.

when(guestbookService.getGuestbooks(0)).thenReturn(list);
위의 문장은 아래와 같이 동작합니다. when( 목객체.목객체메소드호출() ).threnReturn(목객체 메소드가 리턴 할 값)
guestbookService.getGuestbook(0) 이 호출되면 위에서 선언된 list객체가 리턴 되도록 설정합니다.

RequestBuilder reqBuilder
 = MockMvcRequestBuilders.get("/guestbooks").contentType(MediaType.APPLICATION_JSON);

MockMvcRequestBuilders를 이용해 MockMvc에게 호출할 URL을 생성합니다.

get(“/guestbooks”) GET 방식으로 /guestbooks 경로를 호출하라는 의미입니다.

contentType(MediaType.APPLICATION_JSON); application/json 형식으로 api를 호출합니다.

즉 2가지가 합치면 application/json형식으로 /guestbooks를 GET방식으로 호출한다는 것을 뜻합니다. 이러한 URL정보를 가진 reqBuilder를 생성합니다.

mockMvc.perform(reqBuilder).andExpect(status().isOk()).andDo(print());

mockMvc.perform(reqBuilder)reqBuilder에 해당하는 URL에 대한 요청을 보냈다는 것을 의미합니다.

andExpect(status().isOk())mockMvc에 의해 URL이 실행되고 상태코드값이 200이 나와야 한다는 것을 의미합니다. andDo(print())는 처리 내용을 출력하게 됩니다.

여기까지 실행되면 화면에 다음과 같은 결과가 출력되면서 테스트가 성공하게 됩니다.

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /guestbooks
       Parameters = {}
          Headers = {Content-Type=[application/json]}

Handler:
             Type = kr.or.connect.guestbook.controller.GuestbookApiController

           Method = public java.util.Map<java.lang.String, java.lang.Object> 

kr.or.connect.guestbook.controller.GuestbookApiController.list(int)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Content-Type=[application/json;charset=UTF-8]}
     Content type = application/json;charset=UTF-8
             Body =
 {"pageStartList":[],"count":0,"list":[{"id":1,"name":"kim","content":"hello","regdate":1581605377204}]}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

.andExpect(jsonPath("$.list[0].name").value("kim")) 과 같은 문장을 사용하여 Json 결과에 “name”:”kim”이 있을 경우에만 성공이 될 수 있도록 할 수도 있습니다. 이 경우 jsonPath에 대한 라이브러리가 pom.xml파일에 추가 되야 합니다.

<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
    <version>2.4.0</version>
</dependency>

라이브러리 추가 후 test 클래스에 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 를 import 해주어야 합니다.

verify(guestbookService).getGuestbooks(0);

Guestbook 목객체의 getGuestbooks(0)메소드가 호출했다면 검증은 성공하게 됩니다.

@Test
    public void deleteGuestbook() throws Exception {
        Long id = 1L;

        when(guestbookService.deleteGuestbook(id, "127.0.0.1")).thenReturn(1);

        RequestBuilder reqBuilder = MockMvcRequestBuilders.delete("/guestbooks/" + id).contentType(MediaType.APPLICATION_JSON);
        mockMvc.perform(reqBuilder).andExpect(status().isOk()).andDo(print());

        verify(guestbookService).deleteGuestbook(id, "127.0.0.1");
    }

위의 코드는 방명록을 삭제하는 web api를 테스트하고 있습니다.

RequestBuilder reqBuilder = MockMvcRequestBuilders.delete("/guestbooks/" + id).contentType(MediaType.APPLICATION_JSON);

“/guestbooks/” + id 경로를 DELETE방식으로 호출하기 위한 경로 정보를 가지고 있는 reqBuilder객체를 생성합니다.

mockMvc.perform(reqBuilder).andExpect(status().isOk()).andDo(print());

reqBuilder에 해당하는 URL을 호출한 후, 상태 코드가 200일 경우 성공합니다. 그리고 결과를 출력하게 됩니다.

verify(guestbookService).deleteGuestbook(id, "127.0.0.1");

guestbookService 목객체의 deleteGuestbook(id, “127.0.0.1”)메소드가 Web API가 동작하면서 호출되었다면 성공하게 됩니다.


참고 : https://www.boostcourse.org/web326/lecture/59389?isDesc=false

댓글남기기