SpringBoot 1.4.0 Test 적용하기 (2)

Spring·2016.09.08 21:22


2. @WebMvcTest

  • DataJpa 어노테이션이 Repository (Dao) 에 대한 테스트라면 WebMvcTest는 Controller을 위한 테스트 어노테이션이다.

  • Scan 대상은 아래와 같다.

    • @Controller
    • @ControllerAdvice
    • @JsonComponent
    • Filter
    • WebMvcConfigurer and HandlerMethodArgumentResolver
  • MockMvc를 자동으로 지원하고 있어 별도의 HTTP 서버 없이 Controller 테스트를 진행할 수 있다.

  • 사용법 역시 간단하다.

@RunWith(SpringRunner.class)
@WebMvcTest(HomeController.class)
public class WebMvcTest {

    @Autowired
    private MockMvc mvc;

    @Test
    public void test_샘플() throws Exception {
        this.mvc.perform(get("/hello").accept(MediaType.TEXT_PLAIN)) // /hello 라는 url로 text/plain 타입을 요청
                .andExpect(status().isOk()) // 위 요청에 따라 결과가 status는 200이며
                .andExpect(content().string("Hello World"));  // response body에 "Hello World" 가 있는지 검증
    }
}

2.1 상황1

  • 호출한 URL의 View를 검증한다.
  • 시작페이지 구성을 위해 "/" 를 요청하면 home.ftl을 전달하는지 테스트한다.
  • code
    @Test
    public void test_View검증() throws Exception {
        this.mvc.perform(get("/")) // /로 url 호출
                .andExpect(status().isOk()) // 위 요청에 따라 결과가 status는 200이며
                .andExpect(view().name("home"));  // 호출한 view의 이름이 home인지 확인 (확장자는 생략)
    }

2.2 상황2

  • 호출한 URL의 View와 Model 데이터를 검증한다.
  • 아직 Service Layer의 상세스펙은 나온 상태가 아니다.
  • 시작페이지 구성을 위해 "/" 를 요청하면 home.ftl과 Job, Tect, Essay List를 전달하는지 테스트한다.
  • code
// service의 상세스펙이 나오지 않았으니 인터페이스로 service를 먼저 선언한다.
public interface PostService {

    List<Job> getJobList();
    List<Tech> getTechList();
    List<Essay> getEssayList();
}

    // 테스트 코드
    
    @MockBean // postService에 가짜 Bean을 등록
    private PostService postService;
    
    @Test
    public void test_Model검증및ServiceMocking() throws Exception {
        Job[] jobs = {new Job("잡플래닛", LocalDateTime.now(), new ArrayList<>())};
        Tech[] techs = {new Tech("OKKY", LocalDateTime.now(), new ArrayList<>())};
    
        given(this.postService.getJobList()) // this.postService.getJobList 메소드를 실행하면
                .willReturn(Arrays.asList(jobs)); // Arrays.asList(jobs) 를 리턴해줘라.
    
        given(this.postService.getTechList())
                .willReturn(Arrays.asList(techs));
    
        given(this.postService.getEssayList())
                .willReturn(new ArrayList<>());
    
        mvc.perform(get("/"))
                .andExpect(status().isOk())
                .andExpect(view().name("home"))
                .andExpect(model().attributeExists("jobs")) // model에 "jobs" 라는 key가 존재하는지 확인
                .andExpect(model().attribute("jobs", IsCollectionWithSize.hasSize(1))) // jobs model의 size가 1인지 확인
                .andExpect(model().attribute("techs", contains(techs[0]))) // techs model이 "OKKY" 라는 객체를 가지고 있는지 확인
                .andExpect(model().attribute("essays", is(empty()))); // 빈 Collection인지 확인
    }

2.3 상황3

  • Job 데이터를 입력받고 "/" 로 redirect 시킨다.
  • 아직 Service Layer의 상세스펙은 나온 상태가 아니므로 reqeust, response 결과가 정상적인지만 확인한다.
  • Job 스펙에 맞춰 content가 parameter에 포함된다.
  • code
    // controller 코드
    @RequestMapping(value = "/job", method = RequestMethod.POST)
    public String addJob(Job job) {
        postService.addJob(job);
        return "redirect:/";
    }

    // test 코드
    @Test
    public void test_Job데이터입력하기() throws Exception {
        mvc.perform(post("/job")
                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
                .param("content", "많이 와주세요! http://jojoldu.tistory.com"))
                .andExpect(status().is3xxRedirection()) // 302 redirection이 발생했는지 확인
                .andExpect(header().string("Location", "/")) // location이 "/" 인지 확인
                .andDo(MockMvcResultHandlers.print()); // test 응답 결과에 대한 모든 내용 출력
    }    

  • .param() 외에도 .header(), cookie(), sessionAttr() 등의 메소드들이 지원되니 꼭 사용해보길 바란다!

2.4 상황4

  • @ResponseBody를 통해 Json 데이터를 리턴시킨다.
  • Ajax 요청에 대응하기 위해 getJob 메소드는 Job 객체의 Json 형태를 리턴시킨다.
  • 정상적으로 저장되었는지 확인할 수 있어야 한다.
  • code
    // Controller 코드
    @RequestMapping(value="/job/{idx}")
    @ResponseBody
    public Job getJob(@PathVariable long idx) {
        return this.postService.getJob(idx);
    }
        
    // test 코드
    @Test
    public void test_Json결과비교하기() throws Exception {
        Job job = new Job("많이 와주세요! http://jojoldu.tistory.com", LocalDateTime.now(), new ArrayList<>());

        given(this.postService.getJob(1)) // getJob 메소드에 인자값 1이 입력될 경우
                .willReturn(job); // job 객체를 리턴한다. 이럴경우 service 구현체가 없어도 테스트가 가능하다.

        mvc.perform(get("/job/1").accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.content").value("많이 와주세요! http://jojoldu.tistory.com")) // json 데이터중 content 속성의 값을 비교한다.
                .andDo(MockMvcResultHandlers.print());
    }
  • 일반적으로 Json 데이터를 테스트 코드상에서 비교하려면 Json 전체 데이터가 필요한데, update date같이 날짜가 있는 경우엔 전체 데이터 비교가 힘들다. 때문에 Json 데이터 중 일부의 데이터만 비교하는것이 좋다.
  • $.content 코드는 일반적으로 jsonPath 라는 문법으로 불린다. jsonPath를 사용하여 content 값만 비교하였다.
  • 자세한 사용법은 링크를 참고한다.

2.5 상황5

  • Job 데이터 조회시 데이터가 없을 경우 NotFound Exception을 발생시킨다.
  • NotFoundExcption은 Job/Tech/Essay 만을 나타낼수 있도록, PostNotFoundException 이라는 새로운 Exception으로 처리한다.
  • code
// PostNotFound Exception
@ResponseStatus(HttpStatus.NOT_FOUND) // 404 NOT_FOUND status
public class PostNotFoundException extends RuntimeException{ // 직접 생성한 Exception

    public PostNotFoundException(long idx) {
        super("could not find post '" + idx + "'.");
    }
}

    // Controller의 메소드
    @RequestMapping(value="/job/{idx}")
    public Job getJob(@PathVariable long idx) {
        return Optional.ofNullable(this.postService.getJob(idx))
                .orElseThrow(() -> new PostNotFoundException(idx)); // this.postService.getJob(idx)가 null일 경우 PostNotFoundException 발생
    }
    
    //테스트 코드
    @Test
    public void test_Exception체크() throws Exception {
        given(this.postService.getJob(1)) // getJob 메소드에 인자값 1이 입력될 경우
                .willReturn(null); // exception 발생을 위해 null 리턴

        mvc.perform(get("/job/1")) // /job/1 을 호출할 경우
                .andExpect(status().isNotFound()); // not found exception이 나오는지 아닌지 테스트
    }    

Controller와 Repository (Dao)의 테스트가 끝이 났다. 위 방삭을 이용하면 기본적인 테스트 코드는 작성가능하다고 생각한다. 




네이버 페이로 후원 (응원) 할 수 있어요!

(모바일에서만 지원되요 ㅠ)







IntelliJ & 안드로이드 스튜디오의 기본기를 배우고 싶다면 아래 영상을 참고해보세요!

Posted by 창천향로 창천향로

태그