구현 Package 및 Class
Event 목록 조회 API 구현
Event 목록을 조회하는 API를 구현해 보겠다.
테스트 해야할 것들은 다음과 같다.
- 이벤트를 30개를 만들고, 10개 사이즈로 두번째 페이지 조회, pageable 확인
- Page 링크 정보 추가
- 각각의 Event로 가는 링크(self) 및 profile 링크 추가
- query-events 문서화
이벤트를 30개를 만들고, 10개 사이즈로 두번째 페이지 조회, pageable 확인
이벤트를 30개를 만들고, 이벤트 목록 출력시에 한 페이지에 10개씩 해서, 두 번째 페이지를 보여주도록 구현해보자.
EventControllerTests
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| // import 생략
import java.util.stream.IntStream; // 추가
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; // 추가
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
@ActiveProfiles("test")
public class EventControllerTests {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
@Autowired
EventRepository eventRepository; // 추가
// 이 외 테스트 코드 생략
@Test
@TestDescription("30개의 이벤트를 10개씩 두번째 페이지 조회하기")
public void queryEvents() throws Exception{
// Given
IntStream.range(0, 30).forEach(this::genertateEvent);
// When
this.mockMvc.perform(get("/api/events")
.param("page","1")
.param("size","10")
.param("sort","name,DESC")
)
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("pageable").exists())
;
}
private void genertateEvent(int index) {
Event event = Event.builder()
.name("event " + index)
.description("test event")
.build();
this.eventRepository.save(event);
}
}
|
IntStream.range(0, 30).forEach(this::genertateEvent);
를 통해 이벤트를 30개 만들어 준다."name"
: "event 1~30"
"decription"
: "test event"
- MockMVC를 통해
"/api/events"
경로로 Get
요청을 보낸다.param()
메서드를 통해 파라미터 값을 보낸다.
- 페이징과 정렬은 스프링 데이터 JPA가 제공하는
Pageable
를 이용해서 구현할 수 있다.- page : 1은 두 번째 페이지 (0부터 시작)
- size : 한 페이지에 10개씩 (기본값 20)
- sort : (
"name"
,"DESC"
) 👉 이름을 내림차순으로 정렬 (property,property)
EventController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // import 생략
import org.springframework.data.domain.Pageable; // 추가
@Controller
@RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_UTF8_VALUE)
public class EventController {
private final EventRepository eventRepository;
private final ModelMapper modelMapper;
private final EventValidator eventValidator;
public EventController(EventRepository eventRepository, ModelMapper modelMapper, EventValidator eventValidator) {
this.eventRepository = eventRepository;
this.modelMapper = modelMapper;
this.eventValidator = eventValidator;
}
// 이 외 코드 생략
@GetMapping // 추가
public ResponseEntity queryEvents(Pageable pageable) {
return ResponseEntity.ok(this.eventRepository.findAll(pageable));
}
}
|
결과(성공)
- 이벤트가 총 30개 생성 되고, pageNumber는 1(두번째 페이지), pageSize는 10으로 pageable된 것을 확인 할 수 있다.
Page 링크 정보 추가
page를 Resource로 바꿔서 링크 정보를 추가해야하는데, 이 때 SpringData JPA가 제공해주는 PagedResourceAssembler
가 있다.
EventController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| // import 생략
import org.springframework.data.web.PagedResourcesAssembler;
//import org.springframework.hateoas.PagedResources;
//import org.springframework.hateoas.Resource;
@Controller
@RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_UTF8_VALUE)
public class EventController {
private final EventRepository eventRepository;
private final ModelMapper modelMapper;
private final EventValidator eventValidator;
public EventController(EventRepository eventRepository, ModelMapper modelMapper, EventValidator eventValidator) {
this.eventRepository = eventRepository;
this.modelMapper = modelMapper;
this.eventValidator = eventValidator;
}
// 이 외 코드 생략
@GetMapping
public ResponseEntity queryEvents(Pageable pageable, PagedResourcesAssembler<Event> assembler) { // 수정
Page<Event> page = this.eventRepository.findAll(pageable); // 수정
var pagedResources = assembler.toResource(page); // 수정
//PagedResources<Resource<Event>> pagedResources = assembler.toResource(page);
return ResponseEntity.ok(pagedResources);
}
}
|
- SpringData JPA가 제공해주는
PagedResourceAssembler
를 통해 page
를 Resource
화 해준다. - Java에서 제공하는
var
타입으로 긴 코드를 간소화 할 수 있다.
EventControllerTests
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| @RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
@ActiveProfiles("test")
public class EventControllerTests {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
@Autowired
EventRepository eventRepository;
// 이 외 테스트 코드 생략
@Test
@TestDescription("30개의 이벤트를 10개씩 두번째 페이지 조회하기")
public void queryEvents() throws Exception{
// Given
IntStream.range(0, 30).forEach(this::genertateEvent);
// When
this.mockMvc.perform(get("/api/events")
.param("page","1")
.param("size","10")
.param("sort","name,DESC")
)
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("page").exists()) // 수정
;
}
private void genertateEvent(int index) {
Event event = Event.builder()
.name("event " + index)
.description("test event")
.build();
this.eventRepository.save(event);
}
}
|
결과 (성공)
"_embedded"
안에 "eventList"
들어간 것을 확인할 수 있고"page"
페이지 정보와 "_links"
내에 이전/다음("prev"
,"next"
), 처음("first"
), 마지막("last"
) 페이지로 가는 링크들도 확인해 볼 수 있다.
각각의 Event로 가는 링크(self) 및 profile 링크 추가, query-events 문서화
이벤트 목록 페이지 자체의 self
링크는 있지만, 각각의 이벤트 자신의 self
링크는 없다. 이를 추가해 주자.
EventController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| @Controller
@RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_UTF8_VALUE)
public class EventController {
private final EventRepository eventRepository;
private final ModelMapper modelMapper;
private final EventValidator eventValidator;
public EventController(EventRepository eventRepository, ModelMapper modelMapper, EventValidator eventValidator) {
this.eventRepository = eventRepository;
this.modelMapper = modelMapper;
this.eventValidator = eventValidator;
}
// 이 외 코드 생략
@GetMapping
public ResponseEntity queryEvents(Pageable pageable, PagedResourcesAssembler<Event> assembler) {
Page<Event> page = this.eventRepository.findAll(pageable);
var pagedResources = assembler.toResource(page, e -> new EventResource(e)); // 수정
pagedResources.add(new Link("/docs/index.html#resources-events-list").withRel("profile")); // 추가
return ResponseEntity.ok(pagedResources);
}
}
|
- 람다 표현식으로 간단하게 각각의 이벤트에 대한
self
링크를 추가해 줄 수 있다. profile
로 가는 링크를 추가해준다.
EventControllerTests
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| @RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
@ActiveProfiles("test")
public class EventControllerTests {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
@Autowired
EventRepository eventRepository;
// 이 외 테스트 코드 생략
@Test
@TestDescription("30개의 이벤트를 10개씩 두번째 페이지 조회하기")
public void queryEvents() throws Exception{
// Given
IntStream.range(0, 30).forEach(this::genertateEvent);
// When
this.mockMvc.perform(get("/api/events")
.param("page","1")
.param("size","10")
.param("sort","name,DESC")
)
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("page").exists())
.andExpect(jsonPath("_embedded.eventList[0]._links.self").exists())
.andExpect(jsonPath("_links.self").exists())
.andExpect(jsonPath("_links.profile").exists())
.andDo(document("query-events")
// 문서화 생략
)
;
}
private void genertateEvent(int index) {
Event event = Event.builder()
.name("event " + index)
.description("test event")
.build();
this.eventRepository.save(event);
}
}
|
.andDo(document("query-events"))
로 문서화 시에 이벤트 생성 API (createEvent()
)처럼 문서화 해줘야하지만, 그 과정은 생략하겠다.
결과 (성공)
Event 조회 API 구현
이번에는 이벤트 한 건에 대한 데이터를 조회하는 API를 구현해 보겠다.
테스트 해야할 것들은 다음과 같다.
- 존재하는 이벤트 리소스 확인
- 존재하지 않는 이벤트 404응답 확인
- get-an-event 문서화
존재하는 이벤트 리소스 확인 , 존재하지 않는 이벤트 404응답 확인
이벤트를 조회할 때 존재하는 이벤트에 대한 리소스(데이터+링크)를 확인할 수 있어야 하고,
존재하지 않는 이벤트를 조회할 때는 Not Found(404)
응답을 확인 할 수 있어야 한다.
EventControllerTests
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| @RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
@ActiveProfiles("test")
public class EventControllerTests {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
@Autowired
EventRepository eventRepository;
// 이 외 테스트 코드 생략
private Event genertateEvent(int index) {
Event event = Event.builder()
.name("event " + index)
.description("test event")
.build();
return this.eventRepository.save(event);
}
@Test // [1]
@TestDescription("기존의 이벤트를 하나 조회하기")
public void getEvent() throws Exception{
// Given
Event event = this.genertateEvent(100);
// When & Then
this.mockMvc.perform(get("/api/events/{id}", event.getId()))
.andExpect(status().isOk())
.andExpect(jsonPath("name").exists())
.andExpect(jsonPath("id").exists())
.andExpect(jsonPath("_links.self").exists())
.andExpect(jsonPath("_links.profile").exists())
.andDo(document("get-an-event"))
;
}
@Test // [2]
@TestDescription("없는 이벤트를 조회했을 때 404 응답받기")
public void getEvent404() throws Exception{
// When & Then
this.mockMvc.perform(get("/api/events/11883"))
.andExpect(status().isNotFound());
}
}
|
[1]
: 존재하는 이벤트 하나를 조회하는 테스트이다.- 먼저 이벤트(
100
)를 하나 생성한다. "/api/events/100"
경로로 Get
요청을 보낸다.- 응답 상태를 확인하고,
name
,id
데이터와 self
,profile
링크 정보가 있는지 확인한다. "get-an-event"
로 문서화 한다.
[2]
: 존재하지 않는 이벤트를 조회하여 Not Found(404)
응답을 얻는 테스트이다.- 이벤트를 생성하는 과정을 생략한다.
"/api/events/11883"
라는 존재하지 않는 아이디(11883
)에 대한 Get
요청을 보낸다.Not Found(404)
응답을 확인한다.
EventController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| @Controller
@RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_UTF8_VALUE)
public class EventController {
private final EventRepository eventRepository;
private final ModelMapper modelMapper;
private final EventValidator eventValidator;
public EventController(EventRepository eventRepository, ModelMapper modelMapper, EventValidator eventValidator) {
this.eventRepository = eventRepository;
this.modelMapper = modelMapper;
this.eventValidator = eventValidator;
}
// 이 외 코드 생략
@GetMapping("/{id}")
public ResponseEntity getEvent(@PathVariable Integer id){
Optional<Event> optionalEvent = this.eventRepository.findById(id);
if (optionalEvent.isEmpty()) {
return ResponseEntity.notFound().build();
}
Event event = optionalEvent.get();
EventResource eventResource = new EventResource(event);
eventResource.add(new Link("/docs/index.html#resources-events-get").withRel("profile"));
return ResponseEntity.ok(eventResource);
}
}
|
@PathVariable
- 경로 변수를 표시하기 위해 메서드의 매개변수에 사용된다.
- 경로 변수는 중괄호
{}
로 둘러싸인 값을 나타낸다.
Optional
타입으로 this.eventRepository.findById(id);
JPA 메서드를 통해 DB에서 id
에 대한 데이터를 매핑하여 찾는다.Optional
클래스는 null이 올 수 있는 값을 감싸는 Wrapper 클래스로 NPE(NullPointerException)
을 방지한다.
- 객체 값이 비어있으면
Not Found(404)
응답 리턴 - 값이 있으면
event
객체에 get()
을 통해 담아주고event
객체를 EventResource
타입으로 리소스화 해준다.- 이벤트에 해당하는
profile
링크를 eventResource
에 추가해준다.
eventResource
객체를 리턴한다.
결과 (성공)
Event 수정 API 구현
만든 이벤트를 수정하는 API를 구현해 보자.
테스트 해야할 것들은 다음과 같다.
- 정상적으로 수정한 경우 이벤트 리소스 응답
- 입력 값이 잘못되어 있는 경우 이벤트 수정 실패
- 도메인 로직으로 데이터 검증 실패시,
Bad Request(400)
응답 확인 - 입력 데이터(바인딩하는 데이터)가 이상한 경우
Bad Request(400)
응답 확인
- 수정하려는 이벤트가 없는 경우
Not Found(404)
응답 확인
정상적으로 수정한 경우 이벤트 리소스 응답, 문서화
EventControllerTests
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
| @RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
@ActiveProfiles("test")
public class EventControllerTests {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
@Autowired
EventRepository eventRepository;
@Autowired
ModelMapper modelMapper; //추가
// 이 외 테스트 코드 생략
private Event genertateEvent(int index) {
Event event = Event.builder()
.name("event " + index)
.description("test event")
.beginEnrollmentDateTime(LocalDateTime.of(2024,02,19,14,21))
.closeEnrollmentDateTime(LocalDateTime.of(2024,02,20,14,21))
.beginEventDateTime(LocalDateTime.of(2024,02,21,14,21))
.endEventDateTime(LocalDateTime.of(2024,02,22,14,21))
.basePrice(100)
.maxPrice(200)
.limitOfEnrollment(100)
.location("강남역 D2 스타트업 팩토리")
.free(false)
.offline(true)
.eventStatus(EventStatus.DRAFT)
.build();
return this.eventRepository.save(event);
}
@Test
@TestDescription("이벤트를 정상적으로 수정하기")
public void updateEvent() throws Exception{
// Given
Event event = this.genertateEvent(200);
EventDto eventDto = this.modelMapper.map(event, EventDto.class);
String eventName = "Update Event";
eventDto.setName(eventName);
// When & Then
this.mockMvc.perform(put("/api/events/{id}", event.getId())
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(this.objectMapper.writeValueAsString(eventDto))
)
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("name").value(eventName))
.andExpect(jsonPath("_links.self").exists())
.andDo(document("update-event"))
;
}
}
|
- 이벤트(200)을 생성해준다.
ModelMapper
를 이용하여 event
객체를 eventDto
객체에 덮어준다.name
을 수정("Update Event"
으로) 해준다."/api/events/{id}"
경로로 Put
해준다.ObjectMapper
를 통해 eventDto
객체를 JSON 값으로 보낸다.
name
이 "Update Event"
로 수정되었는지 확인한다.self
,profile
링크가 있는지 확인한다."update-event"
로 문서화 한다.
도메인 로직으로 데이터 검증 실패시, Bad Request(400)
응답 확인
EventControllerTests
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
| @RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
@ActiveProfiles("test")
public class EventControllerTests {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
@Autowired
EventRepository eventRepository;
@Autowired
ModelMapper modelMapper; //추가
// 이 외 테스트 코드 생략
private Event genertateEvent(int index) {
Event event = Event.builder()
.name("event " + index)
.description("test event")
.beginEnrollmentDateTime(LocalDateTime.of(2024,02,19,14,21))
.closeEnrollmentDateTime(LocalDateTime.of(2024,02,20,14,21))
.beginEventDateTime(LocalDateTime.of(2024,02,21,14,21))
.endEventDateTime(LocalDateTime.of(2024,02,22,14,21))
.basePrice(100)
.maxPrice(200)
.limitOfEnrollment(100)
.location("강남역 D2 스타트업 팩토리")
.free(false)
.offline(true)
.eventStatus(EventStatus.DRAFT)
.build();
return this.eventRepository.save(event);
}
@Test
@TestDescription("입력값이 비어있는 경우에 이벤트 수정 실패")
public void updateEvent400_Empty() throws Exception{
// Given
Event event = this.genertateEvent(200);
EventDto eventDto = new EventDto();
// When & Then
this.mockMvc.perform(put("/api/events/{id}", event.getId())
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(this.objectMapper.writeValueAsString(eventDto))
)
.andDo(print())
.andExpect(status().isBadRequest());
}
}
|
- 이벤트(200)을 생성해준다.
eventDto
객체를 생성하되, 값을 넣어주지 않는다.eventDto
객체를 보내주고 Bad Request(400)
응답을 확인한다.
입력 데이터(바인딩하는 데이터)가 이상한 경우 Bad Request(400)
응답 확인
EventControllerTests
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
| @RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
@ActiveProfiles("test")
public class EventControllerTests {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
@Autowired
EventRepository eventRepository;
@Autowired
ModelMapper modelMapper; //추가
// 이 외 테스트 코드 생략
private Event genertateEvent(int index) {
Event event = Event.builder()
.name("event " + index)
.description("test event")
.beginEnrollmentDateTime(LocalDateTime.of(2024,02,19,14,21))
.closeEnrollmentDateTime(LocalDateTime.of(2024,02,20,14,21))
.beginEventDateTime(LocalDateTime.of(2024,02,21,14,21))
.endEventDateTime(LocalDateTime.of(2024,02,22,14,21))
.basePrice(100)
.maxPrice(200)
.limitOfEnrollment(100)
.location("강남역 D2 스타트업 팩토리")
.free(false)
.offline(true)
.eventStatus(EventStatus.DRAFT)
.build();
return this.eventRepository.save(event);
}
@Test
@TestDescription("입력값이 잘못된 경우에 이벤트 수정 실패")
public void updateEvent400_Wrong() throws Exception{
// Given
Event event = this.genertateEvent(200);
EventDto eventDto = this.modelMapper.map(event, EventDto.class);
eventDto.setBasePrice(20000);
eventDto.setMaxPrice(1000);
// When & Then
this.mockMvc.perform(put("/api/events/{id}", event.getId())
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(this.objectMapper.writeValueAsString(eventDto))
)
.andDo(print())
.andExpect(status().isBadRequest());
}
}
|
- 이벤트(200)을 생성해준다.
event
객체 값을 eventDto
덮어주고, basePrice
값과 maxPrice
값을 비즈니스 로직상 잘못된 값으로 수정한다.eventDto
객체를 보내주고 Bad Request(400)
응답을 확인한다.
수정하려는 이벤트가 없는 경우 Not Found(404)
응답 확인
EventControllerTests
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
| @RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
@ActiveProfiles("test")
public class EventControllerTests {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
@Autowired
EventRepository eventRepository;
@Autowired
ModelMapper modelMapper; //추가
// 이 외 테스트 코드 생략
private Event genertateEvent(int index) {
Event event = Event.builder()
.name("event " + index)
.description("test event")
.beginEnrollmentDateTime(LocalDateTime.of(2024,02,19,14,21))
.closeEnrollmentDateTime(LocalDateTime.of(2024,02,20,14,21))
.beginEventDateTime(LocalDateTime.of(2024,02,21,14,21))
.endEventDateTime(LocalDateTime.of(2024,02,22,14,21))
.basePrice(100)
.maxPrice(200)
.limitOfEnrollment(100)
.location("강남역 D2 스타트업 팩토리")
.free(false)
.offline(true)
.eventStatus(EventStatus.DRAFT)
.build();
return this.eventRepository.save(event);
}
@Test
@TestDescription("존재하지 않는 이벤트 수정 실패")
public void updateEvent404() throws Exception{
// Given
Event event = this.genertateEvent(200);
EventDto eventDto = this.modelMapper.map(event, EventDto.class);
// When & Then
this.mockMvc.perform(put("/api/events/1231231")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(this.objectMapper.writeValueAsString(eventDto))
)
.andDo(print())
.andExpect(status().isNotFound());
}
}
|
- 이벤트(200)을 생성해준다.
"/api/events/1231231"
라는 존재하지 않는 id
값으로 Put
요청을 보낸다.Not Found(404)
응답을 확인한다.
EventController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| @Controller
@RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_UTF8_VALUE)
public class EventController {
private final EventRepository eventRepository;
private final ModelMapper modelMapper;
private final EventValidator eventValidator;
public EventController(EventRepository eventRepository, ModelMapper modelMapper, EventValidator eventValidator) {
this.eventRepository = eventRepository;
this.modelMapper = modelMapper;
this.eventValidator = eventValidator;
}
// 이 외 코드 생략
private ResponseEntity badRequest(Errors errors){
return ResponseEntity.badRequest().body(new ErrorsResource(errors));
}
@PutMapping("/{id}")
public ResponseEntity updateEvent(@PathVariable Integer id,
@RequestBody @Valid EventDto eventDto,
Errors errors) {
Optional<Event> optionalEvent = this.eventRepository.findById(id);
if (optionalEvent.isEmpty()){
return ResponseEntity.notFound().build();
}
// 도메인 로직 검증
if(errors.hasErrors()){
return badRequest(errors);
}
// 비즈니스 로직 검증
this.eventValidator.validate(eventDto, errors);
if (errors.hasErrors()){
return badRequest(errors);
}
Event existingEvent = optionalEvent.get();
//existingEvent.setName(eventDto.getName());
this.modelMapper.map(eventDto, existingEvent);
Event savedEvent = this.eventRepository.save(existingEvent);
EventResource eventResource = new EventResource(savedEvent);
eventResource.add(new Link("/docs/index.html#resources-events-update").withRel("profile"));
return ResponseEntity.ok(eventResource);
}
}
|
@PathVariable
로 경로 변수 id
값을 받아온다.@RequestBody
로 요청 본문에 EventDto
타입의 eventDto
객체를 받는다.@Valid
로 EventDto
의 도메인 로직을 검증하고, 발생한 에러를 errors
객체에 담는다.- DB에서
id
에 대한 데이터를 매핑하여 찾아 optionalEvent
객체에 담는다. optionalEvent
객체 값이 비어있으면 Not Found(404)
응답 리턴.@Valid
로 도메인 로직으로 데이터를 검증하여 발생한 에러가 있을 시, Bad Request(400)
응답 리턴.
(이때, ErrorResource
도 리턴하여, index
링크도 출력)EventValidator
로 비즈니스 로직으로 데이터를 검증하여 발생한 에러가 있을 시, Bad Request(400)
응답 리턴.
(위와 같음)optionalEvent
이 있으면 existingEvent
객체에 담아준다.ModelMapper
로 수정하고 싶은 값(eventDto
객체에 담긴 값)을 existingEvent
객체의 값에 덮어준다.eventRepository
에 저장하고 savedEvent
객체에 담아준다.- 현재 어떠한 트랜잭션에 들어있는것이 아니기 때문에, 변경사항이 자동으로 dirty checking되어 데이터베이스에 반영되지 않기 때문에 명시적으로 save하는 과정이 꼭 필요하다.
- Service를 만들어야 데이터베이스에 반영된다.
savedEvent
를 eventResource
로 리소스화 하고, profile
링크를 추가해 준다.eventResource
를 응답 body에 담아 리턴
결과 (성공)
테스트 코드 리팩토링 (추가)
1
2
3
4
5
6
| @RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
@ActiveProfiles("test")
|
1
2
3
4
5
6
| @Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
@Autowired
ModelMapper modelMapper;
|
EventControllerTests
와 IndexControllerTest
코드를 보면 위와 같이 중복으로 사용하는 코드가 많다.
그리고 그 코드들은 앞으로의 컨트롤러 테스트 코드 작성시에도 똑같이 중복으로 사용될 것이다.
BaseControllerTest
라는 클래스를 만들어 중복되는 코드를 모아주고, 이를 상속받아서 사용하는 식으로 리팩토링하여 코드를 간소화 해보자.
BaseControllerTest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| package me.hantomas.restapi.common;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Ignore;
import org.junit.runner.RunWith;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
@ActiveProfiles("test")
@Ignore
public class BaseControllerTest {
@Autowired
protected MockMvc mockMvc;
@Autowired
protected ObjectMapper objectMapper;
@Autowired
protected ModelMapper modelMapper;
}
|
@Ignore
: Junit이 제공하는 어노테이션으로, 테스트를 가지고있는 클래스로 간주되지 않도록 한다.
EventControllerTests & IndexControllerTest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| package me.hantomas.restapi.events;
import me.hantomas.restapi.common.BaseControllerTest;
import me.hantomas.restapi.common.TestDescription;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import java.time.LocalDateTime;
import java.util.stream.IntStream;
import static org.springframework.restdocs.headers.HeaderDocumentation.*;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
public class EventControllerTests extends BaseControllerTest {
@Autowired
EventRepository eventRepository;
// 테스트 코드 생략
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| package me.hantomas.restapi.index;
import me.hantomas.restapi.common.BaseControllerTest;
import org.junit.Test;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
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;
public class IndexControllerTest extends BaseControllerTest {
@Test
public void index() throws Exception {
this.mockMvc.perform(get("/api/"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("_links.events").exists());
}
}
|
- import문이 눈에 띄게 줄어든것만 봐도 코드가 간소화 된것을 알 수 있다.
결과 (성공)