Spring MVC/01.Spring @MVC

03. Controller에 대한 단위 테스트

  • -
반응형

Controller와 단위 테스트

Model에서 비지니스 로직을 작성할 때 작성된 메서드들이 오류없이 잘 동작하는지 점검하기 위해 단위테스트를 실시했었다. 처음 테스트 케이스를 구축할 때는 번거로웠지만 일단 테스트 케이스들이 준비되면 테스트 자동화로 인해 개발 시간이 훨씬 줄어드는 것을 확인할 수 있었다

그럼 Controller에서의 단위 테스트는 어떨까?

 

Controller에서의 단위 테스트

앞선 포스트에서 handler method를 만들고 잘 동작하는지 매번 스프링 애플리케이션을 실행하고 브라우저를 통해 서버로 요청한 후 결과 페이지가 생각대로 잘 나오는지 확인 해야했다. 이 과정에 시간이 많이 소요되는 것은 물론 제대로 검증하는 것도 매우 힘들다.

이 과정에서 우리가 주로 확인한 내용들은 어떤 것들이었을까?

  • 요청 경로에 대해 적절한 handler method가 호출되는가?
  • 입력 파라미터는 handler method에 잘 전달되는가?
  • model에 설정한 값은 잘 참조 되는가?
  • 요청 결과 페이지는 잘 연결되는가?

등이다. 즉 Model에서 테스트 하던 비지니스 로직과는 사뭇 다르다. 바로 결과를 보는 것이 아니라 HttpServletRequest와 HttpServletResponse의 내용을 검증해봐야 하는데 이들을 만들기는 쉽지 않다.

또 Model의 동작과 큰 차이점은 Model에서는 모든 코드가 우리의 손끝에서 나왔지만 Controller의 동작에는 브라우저나 WAS처럼 우리가 프로그래밍하지 않은 요소가 개입된다는 점이다. 매번 브라우저, WAS를 동작시키며 테스트 하기는 시간도 많이 걸리고 환경/제품에 따라 동작도 약간씩 다를 수 있기 때문에 동일한 결과가 나오지 않을 수 있다.

따라서 Controller의 단위 테스트를 위해서는 MockMvc이라는 객체가 사용된다. MockMvc는 테스트를 위해 브라우저나 WAS의 동작을 똑같이 처리해줄 수 있는 환경이라고 생각하면 된다. MockMvc를 이용해 브라우저에서 발생하는 요청을 가상으로 만들고 컨트롤러가 응답하는 내용을 기반으로 검증을 수행한다.

휴대폰 가게 앞에 놓여있는 가짜 헨드폰을 Mockup이라고 한다. 똑같이 생긴 가짜다.

 

단위 테스트의 기본 틀

다음은 Controller 테스트를 위한 간단한 단위 테스트 모습을 보여준다.

package com.eshome.mvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import com.eshome.mvc.controller.HelloController;

@SpringBootTest
public class ControllerTest {
    @Autowired
    HelloController hController;

    private MockMvc mock;

    @BeforeEach
    
    public void setup() {
        mock = MockMvcBuilders.standaloneSetup(hController).build();
    }

    @Test
    public void welcomeViewTest() throws Exception {
        mock.perform(get("/")).andExpect(status().isOk());
    }
}

 

MockMvc는 매번 테스트가 실행될 때마다 초기화 해주기 위해서 @BeforeEach에서 초기화 처리한다. standalongSetup은 Object ... 형태로 Controller 객체를 받기 때문에 테스트 하려는 여러 Controller를 등록할 수 있다.

또는 WebApplicationContext를 이용하는 webApplicationContextSetup을 사용하면 모든 Controller를 한번에 처리할 수도 있다.

    @Autowired
    WebApplicationContext ctx;

    MockMvc mock;

    @BeforeEach
    public void test() {
        mock = MockMvcBuilders.webAppContextSetup(ctx).build();
    }

 

MockMvc 객체는 크게 3가지 동작을 갖는다. 

  • perform: 요청 즉 가상의 request를 처리한다.
    • 요청은 MockHttpServletRequestBuilder를 통해서 생성한다.
  • expect: 결과 즉 가상의 response에 대해 검증한다.
    • 검증의 항목은 ResultMatchers를 반환하는 handler(), status(), model(), view() 등 메서드에 따른다.
  • do: 테스트 과정에서 콘솔 출력 등 직접 처리할 일을 작성한다.
    • 실제 동작은 ResultHandler를 사용한다.

 

요청 만들기

요청은 MockMvcRequestBuilders의 static 메서드인 get, post, put, delete, fileUpload 등을 이용해서 MockHttpServletRequestBuilder 객체를 생성하는 것에서 시작한다.

MockHttpServletRequestBuilder builder = get("/add");

MockHttpServletRequestBuilder는 ServletRequest를 구성하기에 필요한 다양한 메서드를 제공한다.

메서드명 설명
param / params 요청 파라미터를 설정한다.
header / headers 요청 헤더를 설정한다. 
cookie 쿠키를 설정한다.
requestAttr request scope에 attribute를 설정한다.
flashAttr flash scope에 attribute를 설정한다.
sessionAttr session scope에 attribute를 설정한다.
locale Locale 정보를 설정한다.
characterEncoding 요청의 인코딩 정보를 설정한다.
contentType Enum인 MediaType으로 요청의 컨텐트 타입을 설정한다.
file fileUpload로 ServletRequestBuilder를 생성한 경우 업로드 파일을 지정한다.

위 메서드들은 method chaining을 지원하기 때문에 쭉 연결해서 작성하는 것이 일반적이다. 만들어진 요청은 MockMvc의 perform 메서드에 파라미터로 설정해준다.

MockHttpServletRequestBuilder builder = 
                get("/add")
                .param("a", "4.5").param("b", "3")
                .cookie(new Cookie("name", "홍길동"))
                .locale(Locale.KOREA);
        
mock.perform(builder);

 

검증하기

perform의 결과로 ResultActions가 반환되는데 이 객체의 andExpect 메서드에 ResultMatcher를 넘겨줘서 검증한다. ResultMatcher는 아래의 MockMvcResultMatchers가 가지는 static 메서드를 통해서 얻는다.

메서드명 설명
handler 요청에 매핑된 컨트롤러를 검증한다.
header 응답 헤더의 값을 검증한다.
cookie 응답을 통해 전달된 쿠키를 검증한다.
content 응답의 본문 내용을 검증한다.
view Controller의 handler method가 반환한 view의 이름을 검증한다.
model model에 담긴 attribute 값을 검증한다.
flash flash scope에 담긴 attribute 값을 검증한다.
forwardedUrl / forwardedUrlPattern forward로 이동하는 대상의 경로를 검증한다.
redirectedUrl / redirectedUrlPattern redirect로 이동하는 경우 대상의 경로를 검증한다.
status Http 상태 코드를 이용해 검증한다.

 

ResultActions 역시 method chaining 형태로 작성할 수 있다.

mock.perform(builder)
    .andExpect(handler().handlerType(HelloController.class))
    .andExpect(handler().methodName("add"))
    .andExpect(forwardedUrl("index"))
    .andExpect(header().stringValues("Content-Language", "en"))
    .andExpect(model().attribute("message", "4.5와 3의 합은 7.5입니다."))
    .andExpect(view().name("index"))
    .andExpect(status().is(200));

 

실행하기

실행하기는 검증하기와 마찬가지로 ResultActions의 andDo 메서드를 이용한다. 파라미터로 ResultHandler 를 전달하는데 이것들은 MockMvcResultHandlers에 static 메서드로 정의되어있다.

메서드 명 설명
print 실행 결과를 표준 출력(System.out)을 이용해 출력한다.
mock.perform(builder)
    .andExpect(status().is(200))
    .andDo(print());
MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /add
       Parameters = {a=[4.5], b=[3]}
          Headers = []
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = com.eshome.mvc.controller.HelloController
           Method = com.eshome.mvc.controller.HelloController#add(Double, Integer, Model)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = index
             View = null
        Attribute = message
            value = 4.5와 3의 합은 7.5입니다.

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Language:"en"]
     Content type = null
             Body = 
    Forwarded URL = index
   Redirected URL = null
          Cookies = []

 

다양한 단위테스트 처리해보기

위 내용을 바탕으로 몇 가지 handler method에 대한 단위 테스트를 처리해보자.

public String redir(Model model)

다음은 메서드가 redirect로 다른 리소스를 호출하는 경우의 단위 테스트 예이다.

@RequestMapping("/redir")
public String redir(Model model) {
    model.addAttribute("message", service.sayHello());
    return "redirect:/";
}
@Test
public void redirTest() throws Exception {
    mock.perform(get("/redir"))
        .andExpect(status().is3xxRedirection())
        .andExpect(model().attribute("message", "what"))
        .andExpect(view().name("redirect:/"))
        .andExpect(redirectedUrl("/?message=what"));
}

 

public String onlyPost(Model model)

다음은 post로 호출되어야 하는 메서드에 대한 단위 테스트 예이다.

@RequestMapping(value = "/onlypost", method = RequestMethod.POST)
public String onlyPost(Model model) {
    String attr = service.echo("post 요청에만 응답함");
    model.addAttribute("message", attr);
    return "index";
}
@Test
public void methodTest() throws Exception {
    // get으로 호출한 경우 405 오류가 발생해야 한다. 4XX로 퉁친다
    mock.perform(get("/onlypost"))
        .andExpect(status().is4xxClientError());

    mock.perform(post("/onlypost"))
        .andExpect(status().isOk());
}

 

public String useDto(@ModelAttribute UserInfo user, Model model)

다음은 다양한 파라미터를 처리하는 예이다.

@GetMapping(value = "/dto")
public String useDto(@ModelAttribute UserInfo user, Model model) {
    model.addAttribute("message", user);
    return "index";
}
@Test
public void dtoTest() throws Exception {
    UserInfo user = new UserInfo();
    user.setName("홍길동");
    user.setAge(30);
    List<String> hobbies = new ArrayList<>();
    hobbies.add("sleep");
    hobbies.add("study");
    user.setHobbies(hobbies);

    mock.perform(get("/dto").param("name", "홍길동")
        .param("age", "30")
        .param("hobbies", "sleep", "study"))
        .andExpect(status().isOk())
        .andExpect(model().attribute("message", user));
}

 

public String setCookie(Model model, HttpServletResponse res)

다음은 설정된 쿠키 값을 검증하는 예이다.

@RequestMapping("/cookieMaker")
public String setCookie(Model model, HttpServletResponse res) {
    Cookie cookie1 = new Cookie("userName", "홍길동");
    cookie1.setMaxAge(60 * 5);
    res.addCookie(cookie1);

    model.addAttribute("message", "쿠키 설정 완료");
    return "index";
}
@Test
public void setCookieTest() throws Exception {
    mock.perform(get("/cookieMaker"))
        .andExpect(cookie().value("userName", "홍길동"))
        .andExpect(cookie().maxAge("userName", 60 * 5))
        .andExpect(status().is2xxSuccessful());
}

 

반응형

'Spring MVC > 01.Spring @MVC' 카테고리의 다른 글

06. 파라미터의 formatting  (0) 2020.07.07
05. Handler Interceptor  (1) 2020.07.03
04. 웹과 관련된 설정  (0) 2020.07.02
02. Controller  (0) 2020.06.30
01. Spring MVC 개요  (0) 2020.06.29
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.