Test 1) JSON 응답으로 201이 나오는지 확인.
- Location 헤더에 생성된 이벤트를 조회할 수 있는 URI 담겨 있는지 확인.
- id는 DB에 들어갈 때 자동 생성된 값으로 나오는지 확인.
일단 Test 클래스를 만든다
- 테스트를 할 클래스에서 맥 기준으로 단축키
cmd + shift + T
를 눌러주면 테스트 클래스 생성해준다.
Location URI를 만들건데, 여기서는 HATEOAS
가 제공하는 linkTo()
, methodOn()
을 사용할 것 이다.
객체를 JSON으로 변환 할때, ObjectMapper
를 사용.
EventController 구현
package me.iseunghan.demoinflearnrestapi.events;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import java.net.URI;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
@Controller
@RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_VALUE)
public class EventController {
private final EventRepository eventRepository;
public EventController(EventRepository eventRepository) {
this.eventRepository = eventRepository;
}
@PostMapping
public ResponseEntity createEvent(@RequestBody Event event) {
Event newEvent = this.eventRepository.save(event);
//link를 생성할땐,HATEOAS가 제공하는 linkTo(), methodOn()을 사용
//methodOn()을 사용하지 않은 이유 : URL이 클래스 레벨에 붙었기 때문에 메소드를 호출 안해도 된다.
URI createUri = linkTo(EventController.class).slash(newEvent.getId()).toUri();
return ResponseEntity.created(createUri).body(event); //201응답을 Uri에 담아서 리턴시킨다.
}
}
위에서 methodOn()
을 안쓰고 linkTo(T class)
로 바로 들어간 이유는, 클래스 레벨에 붙은 @RequestMapping
때문이다.
Link를 만들때에는 @PostMapping("/api/events")
처럼 메소드레벨에 붙은 메소드를 호출하는 방식처럼 이루어지는데,
@Controller
public class Controller {
...
@PostMapping("/api/events")
public ResponseEntity createEvent(@RequestBody Event event){
URI createUri = linkTo(methodOn(EventController.class).createEvent(null)).slash(newEvent.getId()).toUri();
...
}
만약 위와 같이 URL이 메소드 레벨에 붙었다면 methodOn()
까지 사용해야 한다.
EventControllerTest 클래스 구현
package me.iseunghan.demoinflearnrestapi.events;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import java.time.LocalDateTime;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* MockMvc
* - 스프링 MVC 테스트 핵심 클래스
* - 웹서버를 띄우지 않고도 스프링 Mvc(DispatherServlet)가
* 요청을 처리하는 과정을 확인할 수 있기 때문에 컨트롤러 테스트용으로 자주 쓰임.
*/
@RunWith(SpringRunner.class)
@WebMvcTest
public class EventControllerTests {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
//MockBean으로 등록하는 이유 : @WebMvcTest는 웹 관련 빈들만 등록해주기 때문에, 직접 등록해야한다.
//(주의) 기존 빈을 테스트용 빈이 대체 한다.
@MockBean
EventRepository eventRepository;
@Test
public void createEvent() throws Exception {
Event event = Event.builder()
.name("Spring")
.description("REST API Development with Spring")
.beginEnrollmentDateTime(LocalDateTime.of(2020, 9, 7, 2, 45))
.closeEnrollmentDateTime(LocalDateTime.of(2020,9,8,2,45))
.beginEventDateTime(LocalDateTime.of(2020,9,9,2,45))
.endEventDateTime(LocalDateTime.of(2020,9,10,2,45))
.basePrice(100)
.maxPrice(200)
.limitOfEnrollment(100)
.location("강남역 1번 출구")
.build();
event.setId(10);
Mockito.when(eventRepository.save(event)).thenReturn(event);
mockMvc.perform(post("/api/events/")
.contentType(MediaType.APPLICATION_JSON)//본문 요청에 json을 담아서 보내고 있다고 알려줌.
.accept(MediaTypes.HAL_JSON)//HAL_JSON으로 받는다.
.content(objectMapper.writeValueAsString(event)))//요청 본문에 json으로 변환후 넣어준다
.andDo(print())//어떤 응답과 요청을 받았는지 확인가능.
.andExpect(status().isCreated())//201요청이 들어왔는지?
.andExpect(jsonPath("id").exists()) //json에 id가 있는지?
.andExpect(header().exists(HttpHeaders.LOCATION)) //header에 Location정보가 담겨있는지
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_VALUE))//content-type에 "application/hal+json"가 나오는지?
;
}
}
mockMvc.perform(post("/api/events/")
post방식으로 "api/events/" URL를 넘겨서 호출한다..contentType(MediaType.APPLICATION_JSON)
본문 요청에 JSON을 담아서 보내고 있다고 명시.accept(MediaType.HAL_JSON)
HAL + JSON 타입으로 받겠다는 의미.content(objectMapper.writeValueAsString(event))
요청 본문에 event 객체를 JSON으로 변환해서 넣어준다..andDo(print())
응답, 요청 print.andExpect(status().isCreate())
201요청이 들어왔는가.andExpect(jsonPath("id").exists())
json에 id가 존재하는가?
@MockBean, Mokito.when().then()
Test도중 Spring에서 어느 의존성도 필요하지 않다면, Mockito의 @Mock을 사용하는 방법이 좋습니다. 왜냐하면, 우리가 바라는 빠르고 의존성없는 unit test로 가는 방향이기 때문입니다. 하지만, test 도중 spring container가 관리하는 bean들 중 하나라도 추가하거나 Mocking하고 싶다면 @MockBean을 선택할 수 있습니다. @MockBean은 ApplicationContext에 mock객체를 추가합니다. 그리고 mock객체는 같은 type의 이미 존재하는 Bean들을 대신할 것입니다. 하지만, @MockBean으로 대신된 context는 다른 context이므로 Spring Boot는 ApplicationContext를 test 초기화 중에 다시 로딩해야합니다.
테스트 실패
테스트 코드에서는 eventRepoitory를 Mock 객체를 전달했는데, @MockBean을 이용해서 생성한 객체의 메소드가 리턴 타입이 객체인 경우 null을 리턴하고, 기본 데이터 타입인 경우 기본 값을 리턴한다. 그리하여, 테스트에서 Controller의 UrI만드는 과정에서
newEvent.getId() 를 호출하는 과정에서 newEvent가 null이기 때문에 NullpointerException이 발생하여 테스트가 깨지게 된다.
이럴땐, Mockito를 이용한다..
Mockito는 Mock 객체의 메서드가 알맞은 값을 리턴하는 스텁을 만들 수 있는 기능을 제공하고 있다. 이 메서드는 when - then의 형식을 띄고
그리하여, when
: eventRepository.save 메소드가 호출 될때! -> thenReturn
: event 객체를 반환한다.
Test 1) JSON 응답으로 201이 나오는지 확인.
- Location 헤더에 생성된 이벤트를 조회할 수 있는 URI 담겨 있는지 확인.
- id는 DB에 들어갈 때 자동 생성된 값으로 나오는지 확인.
일단 Test 클래스를 만든다
- 테스트를 할 클래스에서 맥 기준으로 단축키
cmd + shift + T
를 눌러주면 테스트 클래스 생성해준다.
Location URI를 만들건데, 여기서는 HATEOAS
가 제공하는 linkTo()
, methodOn()
을 사용할 것 이다.
객체를 JSON으로 변환 할때, ObjectMapper
를 사용.
EventController 구현
package me.iseunghan.demoinflearnrestapi.events;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import java.net.URI;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
@Controller
@RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_VALUE)
public class EventController {
private final EventRepository eventRepository;
public EventController(EventRepository eventRepository) {
this.eventRepository = eventRepository;
}
@PostMapping
public ResponseEntity createEvent(@RequestBody Event event) {
Event newEvent = this.eventRepository.save(event);
//link를 생성할땐,HATEOAS가 제공하는 linkTo(), methodOn()을 사용
//methodOn()을 사용하지 않은 이유 : URL이 클래스 레벨에 붙었기 때문에 메소드를 호출 안해도 된다.
URI createUri = linkTo(EventController.class).slash(newEvent.getId()).toUri();
return ResponseEntity.created(createUri).body(event); //201응답을 Uri에 담아서 리턴시킨다.
}
}
위에서 methodOn()
을 안쓰고 linkTo(T class)
로 바로 들어간 이유는, 클래스 레벨에 붙은 @RequestMapping
때문이다.
Link를 만들때에는 @PostMapping("/api/events")
처럼 메소드레벨에 붙은 메소드를 호출하는 방식처럼 이루어지는데,
@Controller
public class Controller {
...
@PostMapping("/api/events")
public ResponseEntity createEvent(@RequestBody Event event){
URI createUri = linkTo(methodOn(EventController.class).createEvent(null)).slash(newEvent.getId()).toUri();
...
}
만약 위와 같이 URL이 메소드 레벨에 붙었다면 methodOn()
까지 사용해야 한다.
EventControllerTest 클래스 구현
package me.iseunghan.demoinflearnrestapi.events;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import java.time.LocalDateTime;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* MockMvc
* - 스프링 MVC 테스트 핵심 클래스
* - 웹서버를 띄우지 않고도 스프링 Mvc(DispatherServlet)가
* 요청을 처리하는 과정을 확인할 수 있기 때문에 컨트롤러 테스트용으로 자주 쓰임.
*/
@RunWith(SpringRunner.class)
@WebMvcTest
public class EventControllerTests {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
//MockBean으로 등록하는 이유 : @WebMvcTest는 웹 관련 빈들만 등록해주기 때문에, 직접 등록해야한다.
//(주의) 기존 빈을 테스트용 빈이 대체 한다.
@MockBean
EventRepository eventRepository;
@Test
public void createEvent() throws Exception {
Event event = Event.builder()
.name("Spring")
.description("REST API Development with Spring")
.beginEnrollmentDateTime(LocalDateTime.of(2020, 9, 7, 2, 45))
.closeEnrollmentDateTime(LocalDateTime.of(2020,9,8,2,45))
.beginEventDateTime(LocalDateTime.of(2020,9,9,2,45))
.endEventDateTime(LocalDateTime.of(2020,9,10,2,45))
.basePrice(100)
.maxPrice(200)
.limitOfEnrollment(100)
.location("강남역 1번 출구")
.build();
event.setId(10);
Mockito.when(eventRepository.save(event)).thenReturn(event);
mockMvc.perform(post("/api/events/")
.contentType(MediaType.APPLICATION_JSON)//본문 요청에 json을 담아서 보내고 있다고 알려줌.
.accept(MediaTypes.HAL_JSON)//HAL_JSON으로 받는다.
.content(objectMapper.writeValueAsString(event)))//요청 본문에 json으로 변환후 넣어준다
.andDo(print())//어떤 응답과 요청을 받았는지 확인가능.
.andExpect(status().isCreated())//201요청이 들어왔는지?
.andExpect(jsonPath("id").exists()) //json에 id가 있는지?
.andExpect(header().exists(HttpHeaders.LOCATION)) //header에 Location정보가 담겨있는지
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_VALUE))//content-type에 "application/hal+json"가 나오는지?
;
}
}
mockMvc.perform(post("/api/events/")
post방식으로 "api/events/" URL를 넘겨서 호출한다..contentType(MediaType.APPLICATION_JSON)
본문 요청에 JSON을 담아서 보내고 있다고 명시.accept(MediaType.HAL_JSON)
HAL + JSON 타입으로 받겠다는 의미.content(objectMapper.writeValueAsString(event))
요청 본문에 event 객체를 JSON으로 변환해서 넣어준다..andDo(print())
응답, 요청 print.andExpect(status().isCreate())
201요청이 들어왔는가.andExpect(jsonPath("id").exists())
json에 id가 존재하는가?
@MockBean, Mokito.when().then()
Test도중 Spring에서 어느 의존성도 필요하지 않다면, Mockito의 @Mock을 사용하는 방법이 좋습니다. 왜냐하면, 우리가 바라는 빠르고 의존성없는 unit test로 가는 방향이기 때문입니다. 하지만, test 도중 spring container가 관리하는 bean들 중 하나라도 추가하거나 Mocking하고 싶다면 @MockBean을 선택할 수 있습니다. @MockBean은 ApplicationContext에 mock객체를 추가합니다. 그리고 mock객체는 같은 type의 이미 존재하는 Bean들을 대신할 것입니다. 하지만, @MockBean으로 대신된 context는 다른 context이므로 Spring Boot는 ApplicationContext를 test 초기화 중에 다시 로딩해야합니다.
테스트 실패
테스트 코드에서는 eventRepoitory를 Mock 객체를 전달했는데, @MockBean을 이용해서 생성한 객체의 메소드가 리턴 타입이 객체인 경우 null을 리턴하고, 기본 데이터 타입인 경우 기본 값을 리턴한다. 그리하여, 테스트에서 Controller의 UrI만드는 과정에서
newEvent.getId() 를 호출하는 과정에서 newEvent가 null이기 때문에 NullpointerException이 발생하여 테스트가 깨지게 된다.
이럴땐, Mockito를 이용한다..
Mockito는 Mock 객체의 메서드가 알맞은 값을 리턴하는 스텁을 만들 수 있는 기능을 제공하고 있다. 이 메서드는 when - then의 형식을 띄고
그리하여, when
: eventRepository.save 메소드가 호출 될때! -> thenReturn
: event 객체를 반환한다.