⚙️ Backend/스프링(Spring) Framework

Junit 4 & Spring Test을 이용한 TDD 환경 세팅

코너(Corner) 2021. 5. 11.
반응형

프레젠테이션 계층의 구현과 테스트

학습 목표 : WAS를 직접 실행하지 않고 Controller를 테스트할 수 있다. (WAS에 의존적이지 않다.)

1. Controller의 작성

스프링 MVC의 Controller는 하나의 클래스 내에서 여러 메서드를 작성하고, @RequestMapping 등을 이용해서 URL을 분기하는 구조로 작성할 수 있기 때문에 하나의 클래스에서 필요한 만큼 메서드의 분기를 이용하는 구조로 작성한다.

과거에는 이 단계에서 Tomcat(WAS)를 실행하고 웹 화면을 만들어서 결과를 확인하는 방식의 코드를 작성해 왔다. 이 방식은 시간도 오래 걸리거니와 테스트를 자동화 하기에 어려움이 많다.

1-1. BoardController 분석 (기획)

작성하기 전에는 반드시 현재 원하는 기능을 호출하는 방식에 대해 다음과 같이 테이블로 정리한 후 코드를 작성하는 것이 좋다.

TASK URL Method Parameter Form URL
전체목록 /board/list GET 방식 처리 - - -
등록처리 /board/register POST 방식 처리 모든 항목 필요 이동
조회 /board/read GET 방식 처리 bno - -
삭제처리 /board/remove POST 방식 처리 bno - 이동
수정처리 /board/modify POST 방식 처리 모든 항목 필요 이동

테이블에서 From 항목은 해당 URL을 호출하기 위해서 별도의 입력화면이 필요하다는 것을 의미한다. 이에 대한 설계는 화면을 구성하는 단계에서 진행할 수 있다.


삭제 처리는 POST방식으로 해야한다. 반드시!

GET방식으로 삭제를 처리하게 된다면 누군가 URL에 delete/45 modify/45 주소창에 이렇게 입력한다면 게시글 삭제 혹은 수정에 대한 권한은 본인 혹은 관리자에게 있기 때문에 타인에 의해서 게시글이 삭제되거나 수정이 되면 안된다.

이럴때는 method 방식을 POST로 요청을 보내도록 하는 form태그와 submit버튼을 새로 지정해주어야 한다.


2. BoardController의 작성

2-1. 목록에 대한 처리와 리스트
2-1-1. BoardController에서 전체 목록을 가져오는 처리를 먼저 작성한다. BoardController는 BoardService 타입의 객체와 같이 연동해야 하므로 의존성에 대한 처리도 같이 진행한다.
package com.koreait.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.koreait.service.BoardService;

import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j;

@Controller
@Log4j
@AllArgsConstructor // 생성자로 service 주입 
@RequestMapping("/board/*")
public class BoardController {

    private BoardService service;

    @GetMapping("/list")
    public void list(Model model) {
        log.info("list");
        model.addAttribute("list",service.getList());
    }

}

oardController 는 BoardService에 대해서 의존적이므로 @AllArgsConstructor를 이용해서 생성자를 만들고 자동으로 주입하도록 한다. (만일 생성자를 만들지 않을 경우에는 @Setter(onMethod_ = {@Autowired}) 를 이용해서 처리한다.)

list()는 나중에 게시물의 목록을 전달해야 하므로 Model을 파라미터로 지정하고, 이를 통해서 BoardServiceImpl 객체의 getList() 결과를 담아 전달한다. (=addAttribute) BoardController 테스트는 스프링의 테스트 기능을 통해서 확인해 볼 수 있다.

2-1-2. src/test/java 에 org.zerock.controller 패키지에 BoardControllerTests 클래스를 선언한다.

img

테스트 코드는 기존과 좀 다르게 진행된다. 그 이유는 웹을 개발할 때 매번 URL을 테스트하기 위해서 Tomcat과 같은 WAS를 실행하는 불편한 단계를 생략하기 위해서다. 스프링의 테스트 기능을 활용하면 개발 당시에 Tomcat(WAS)를 실행하지 않고도 스프링과 웹 URL을 테스트할 수 있다.

WAS를 실행하지 않기 위해서는 약간의 추가적인 코드가 필요하지만 반복적으로 서버를 실행하고 화면에 입력하고, 오류를 수정하는 단계를 줄여줄 수 있기 때문에 Controller를 테스트할 때는 한 번쯤 고려해 볼 만한 방식이다.

2-1-3. BoardControllerTests 클래스에 다음과 같은 코드를 작성한다.
package com.koreait.controller;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
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.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import lombok.Setter;
import lombok.extern.log4j.Log4j;

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration   // Servlet의 ServletContext를 이용하기 위함
@ContextConfiguration({
   "file:src/main/webapp/WEB-INF/spring/root-context.xml",
   "file:src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml"
})
@Log4j
public class BoardControllerTests {
   @Setter(onMethod_ = @Autowired)
   private WebApplicationContext wac;

   // 가짜 MVC
   // 마치 브라우저에서 사용하는 것처럼 만들어서 Controller를 실행해 볼 수 있다.
   private MockMvc mockMvc; // 주입을 받는게 아니라 wac를 통해서 build 하는 것

   @Before   // org.JUnit, 모든 테스트 전에 실행되는 어노테이션
   public void setup() {
      // WebApplicationContext를 통해 ServletContext를 빌드한다.
      // builder가 아니고 builder"s"
      this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); 
   }

   @Test
   public void testList() throws Exception{
      log.info(mockMvc.perform(MockMvcRequestBuilders.get("/board/list"))   // GET방식 URI 호출 
               .andReturn()        // 응답된 결과값을 통해
               .getModelAndView() // Model에 어떤 데이터가 담겨 있는지를 
               .getModelMap());        // Map 형식으로 확인   
      // model을 Map으로 바꿔준다.
   }
}

테스트 클래스의 선언부에는 @WebAppConfiguration 어노테이션을 적용한다. @WebAppConfiguration은 Servlet의 ServletContext를 이용하기 위해서인데, 스프링에서는 WebApplicationContext라는 존재를 이용하기 위해서이다.

@Before 어노테이션이 적용된 setUp()에서는 import 할 때 JUnit을 이용해야 한다. @Before가 적용된 메서드는 모든 테스트 전에 매번 실행되는 메서드가 된다.

MockMvc는 말 그대로 '가짜 mvc'라고 생각하면 된다. 가짜로 URL과 파라미터 등을 브라우저에서 사용하는 것처럼 만들어서 Controller를 실행해 볼 수 있다.

testList()는 MockMvcRequestBuilders라는 존재를 이용해서 GET 방식의 호출을 한다. 이후에는 BoardController의 getList()에서 반환된 결과를 이용해서 Model에 어떤 데이터들이 담겨 있는지 확인한다. Tomcat을 통해서 실행되는 방식이 아니므로 기존의 테스트 코드를 실행하는 것과 동일하게 실행한다.

testList()를 실행한 결과는 데이터베이스에 저장된 게시물들을 볼 수 있다.

img

오류가 뜬다면 아래와 같이 pom.xml에 수정한다.

<dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
</dependency>

 

2-2. 등록 처리와 테스트 구현

2-1-1. BoardController.java 작성
// 전송방식은 forward 방식이 Default로 지정되어 있다.
    @PostMapping("/register")
    public String register(BoardVO board, RedirectAttributes rttr) {
        log.info("register : " + board);
        service.register(board);

        // Flash는 Session처럼 유지 시켜준다.
        // 전송할때 Flash 영역에 잠깐 담아두었다가 전송이 되고나면 사라진다. 

        // 새롭게 등록된 번호를 .jsp에 전달하기 위해서는 request객체에 담아야한다.
        // 하지만, redirect방식으로 전송할 때 request가 초기화된다.
        // 따라서 세션에 있는 Flash영역에 담아놓고
        // 초기화된 request객체에 전달해주면 결과값을 안전하게 이동시킬 수 있다.
        // 이 때 RedirectAttributes를 이용한다.
        rttr.addFlashAttribute("result", board.getBno());

        // 'redirect': 접두어를 사용하면 스프링 MVC가 내부적으로 
        // response.sendRedirect()를 처리해준다.
        return "redirect:/board/list"; 
    }

register() 메서드는 조금 다르게 String을 리턴 타입으로 지정하고, Redirect Attributes를 파라미터로 지정한다. 이는 등록 작업이 끝난 후 다시 목록 화면으로 이동하기 위함인데, 추가적으로 새롭게 등록된 게시물의 번호를 같이 전달하기 위해서 RedirectAttributes를 이용한다.

리턴 시에는 'redirect:' 접두어를 사용하는데 이를 이용하면 스프링 MVC가 내부적으로 response.sendRedirect()를 처리해 주기 때문에 편리하다.

2-2-2. 테스트 작성
   @Test
   public void testRegister() throws Exception {
       log.info(mockMvc.perform(MockMvcRequestBuilders.post("/board/register")
               .param("title", "테스트 새 글 제목")
               .param("content", "테스트 새 글 내용")
               .param("writer", "test123")
//               ).andReturn().getFlashMap());
               ).andReturn().getModelAndView().getViewName());
   }

img


2-3. 조회 처리와 테스트 구현

2-3-1. BoardController.java 작성

// 등록 처리와 유사하게 조회 처리도 BoardController 를 이용해서 처리할 수 있다.
// 특별한 경우가 아니라면 조회는 GET 방식으로 처리하므로, @GetMapping을 이용한다.

    @GetMapping("/read")
    /*
     RequestParam은 객체와 일반 변수가 동시에 있을 때 분리하기 위해 작성한다. (좀 더 명시적 처리)
     * */
    public void get(@RequestParam("bno") Long bno, Model model) {
        log.info("/get");
        model.addAttribute("board",service.get(bno));
    }

// BoardController의 get() 메서드에는 bno 값을 좀 더 명시적으로 처리하는
// @RequestParam을 이용해서 지정한다.
// (파라미터 이름과 변수 이름을 기준으로 동작하기 때문에 생략해도 무방하다.)
// 또한 화면 쪽으로 해당 번호의 게시물을 전달해야 하므로 Model을 파라미터로 지정한다.

2-3-2. 테스트 작성
   @Test
   public void testGet() throws Exception {
       log.info(mockMvc.perform(MockMvcRequestBuilders
               .get("/board/read")
               .param("bno", "8"))
               .andReturn().getModelAndView().getModelMap()
               );
   }
2-3-3. 테스트 결과 확인

img

2-4 수정 및 삭제 처리와 테스트

수정 및 삭제 작업은 등록과 유사하다. 수정의 경우, 변경된 내용을 수집해서 BoardVO 파라미터로 처리하고, BoardService를 호출한다. 수정 작업을 시작하는 화면의 경우에는 GET 방식으로 접근하지만 실제 작업은 POST 방식으로 동작하므로 @PostMapping을 이용해서 처리한다. 삭제의 경우, 반드시 POST 방식으로만 처리한다.

2-4-1. BoardController 메서드 작성.
    // 수정과 삭제는 성공 시 result에 success를 담아서 view에 전달하기.
    // 수정 처리와 테스트 구현
    @PostMapping("/modify")
    public String modify(BoardVO board, RedirectAttributes rttr) {
        log.info("modify : " + board);

        if(service.modify(board)) {
            rttr.addFlashAttribute("result", "success");
        }
        return "redirect:/board/list";
    }

    // 삭제 처리와 테스트 구현
    @GetMapping("/remove")
    public String remove(@RequestParam("bno") Long bno, RedirectAttributes rttr ) {
        log.info("remove...: "+bno);
        if(service.remove(bno)) {
            rttr.addFlashAttribute("result","success");
        }
        return "redirect:/board/list";
    }

수정 : service.modify()는 수정 여부를 boolean으로 처리하므로 이를 이용해서 성공한 경우에만 RedirectAttributes에 추가한다.

삭제 : BoardController의 remove()는 삭제 후 페이지의 이동이 필요하므로 RedirectAttributes를 파라미터로 사용하고 'redirect'를 이용해서 삭제 처리 후에 다시 목록 페이지로 이동한다.

2-4-2. 테스트 코드의 처리
   @Test
   public void testRemove() throws Exception{
       log.info(mockMvc.perform(MockMvcRequestBuilders.get("/board/remove")
               .param("bno", "8")
               ).andReturn().getModelAndView().getViewName());
   }

   @Test
   public void testModify() throws Exception {
       log.info( mockMvc.perform(MockMvcRequestBuilders.post("/board/modify")
               .param("bno", "8")
               .param("title", "수정된 텍스트 제목이다.")
               .param("content", "수정된 테스트 글 내용이다.")
               .param("writer", "test123"))
               .andReturn().getModelAndView().getViewName()
               );
   }

MockMvc를 이용해서 파라미터를 전달할 때는 문자열로만 처리해야 한다.

2-4-3. 테스트 결과 확인

반응형

댓글